feat: full local assets / album sync

This commit is contained in:
shenlong-tanwen
2024-10-17 23:33:00 +05:30
parent a09710ec7b
commit c91a2878dc
87 changed files with 2417 additions and 366 deletions
+60 -9
View File
@@ -1,31 +1,82 @@
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/utils/collection_util.dart';
@immutable
class LocalAlbum {
final int id;
final String localId;
class Album {
final int? id;
final String? localId;
final String? remoteId;
final String name;
final DateTime modifiedTime;
final int? thumbnailAssetId;
const LocalAlbum({
required this.id,
required this.localId,
bool get isRemote => remoteId != null;
bool get isLocal => localId != null;
const Album({
this.id,
this.localId,
this.remoteId,
required this.name,
required this.modifiedTime,
this.thumbnailAssetId,
});
@override
bool operator ==(covariant LocalAlbum other) {
bool operator ==(covariant Album other) {
if (identical(this, other)) return true;
return other.hashCode == hashCode;
return other.id == id &&
other.localId == localId &&
other.remoteId == remoteId &&
other.name == name &&
other.modifiedTime == modifiedTime &&
other.thumbnailAssetId == thumbnailAssetId;
}
@override
int get hashCode {
return id.hashCode ^
localId.hashCode ^
remoteId.hashCode ^
name.hashCode ^
modifiedTime.hashCode;
modifiedTime.hashCode ^
thumbnailAssetId.hashCode;
}
Album copyWith({
int? id,
String? localId,
String? remoteId,
String? name,
DateTime? modifiedTime,
int? thumbnailAssetId,
}) {
return Album(
id: id ?? this.id,
localId: localId ?? this.localId,
remoteId: remoteId ?? this.remoteId,
name: name ?? this.name,
modifiedTime: modifiedTime ?? this.modifiedTime,
thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId,
);
}
@override
String toString() => """
{
id: ${id ?? "-"},
localId: "${localId ?? "-"}",
remoteId: "${remoteId ?? "-"}",
name: $name,
modifiedTime:
$modifiedTime,
thumbnailAssetId: "${thumbnailAssetId ?? "-"}",
}""";
static int compareByLocalId(Album a, Album b) =>
CollectionUtil.compareToNullable(a.localId, b.localId);
static int compareByRemoteId(Album a, Album b) =>
CollectionUtil.compareToNullable(a.remoteId, b.remoteId);
}
@@ -0,0 +1,38 @@
class AlbumETag {
final int? id;
final int albumId;
final int assetCount;
final DateTime modifiedTime;
const AlbumETag({
this.id,
required this.albumId,
required this.assetCount,
required this.modifiedTime,
});
factory AlbumETag.empty() {
return AlbumETag(
albumId: -1,
assetCount: 0,
modifiedTime: DateTime.now(),
);
}
@override
bool operator ==(covariant AlbumETag other) {
if (identical(this, other)) return true;
return other.id == id &&
other.albumId == albumId &&
other.assetCount == assetCount &&
other.modifiedTime == modifiedTime;
}
@override
int get hashCode =>
id.hashCode ^
albumId.hashCode ^
assetCount.hashCode ^
modifiedTime.hashCode;
}
@@ -6,11 +6,11 @@ import 'package:immich_mobile/presentation/modules/theme/models/app_theme.model.
// This model is the only exclusion which refers to entities from the presentation layer
// as well as the domain layer
enum AppSetting<T> {
appTheme<AppTheme>(StoreKey.appTheme, AppTheme.blue),
themeMode<ThemeMode>(StoreKey.themeMode, ThemeMode.system),
darkMode<bool>(StoreKey.darkMode, false);
appTheme<AppTheme>._(StoreKey.appTheme, AppTheme.blue),
themeMode<ThemeMode>._(StoreKey.themeMode, ThemeMode.system),
darkMode<bool>._(StoreKey.darkMode, false);
const AppSetting(this.storeKey, this.defaultValue);
const AppSetting._(this.storeKey, this.defaultValue);
// ignore: avoid-dynamic
final StoreKey<T, dynamic> storeKey;
+26 -18
View File
@@ -12,7 +12,7 @@ enum AssetType {
}
class Asset {
final int id;
final int? id;
final String name;
final String hash;
final int? height;
@@ -32,9 +32,10 @@ class Asset {
bool get isRemote => remoteId != null;
bool get isLocal => localId != null;
bool get isMerged => isRemote && isLocal;
bool get isImage => type == AssetType.image;
const Asset({
required this.id,
this.id,
required this.name,
required this.hash,
this.height,
@@ -49,7 +50,6 @@ class Asset {
});
factory Asset.remote(AssetResponseDto dto) => Asset(
id: 0, // assign a temporary auto gen ID
remoteId: dto.id,
createdTime: dto.fileCreatedAt,
duration: dto.duration.tryParseInt() ?? 0,
@@ -93,29 +93,38 @@ class Asset {
}
Asset merge(Asset newAsset) {
if (newAsset.modifiedTime.isAfter(modifiedTime)) {
final existingAsset = this;
assert(existingAsset.id != null, "Existing asset must be from the db");
final oldestCreationTime =
existingAsset.createdTime.isBefore(newAsset.createdTime)
? existingAsset.createdTime
: newAsset.createdTime;
if (newAsset.modifiedTime.isAfter(existingAsset.modifiedTime)) {
return newAsset.copyWith(
height: newAsset.height ?? height,
width: newAsset.width ?? width,
localId: () => newAsset.localId ?? localId,
remoteId: () => newAsset.remoteId ?? remoteId,
livePhotoVideoId: newAsset.livePhotoVideoId ?? livePhotoVideoId,
id: newAsset.id ?? existingAsset.id,
localId: () => existingAsset.localId ?? newAsset.localId,
remoteId: () => existingAsset.remoteId ?? newAsset.remoteId,
width: newAsset.width ?? existingAsset.width,
height: newAsset.height ?? existingAsset.height,
createdTime: oldestCreationTime,
);
}
return copyWith(
height: height ?? newAsset.height,
width: width ?? newAsset.width,
localId: () => localId ?? newAsset.localId,
remoteId: () => remoteId ?? newAsset.remoteId,
livePhotoVideoId: livePhotoVideoId ?? newAsset.livePhotoVideoId,
return existingAsset.copyWith(
localId: () => existingAsset.localId ?? newAsset.localId,
remoteId: () => existingAsset.remoteId ?? newAsset.remoteId,
width: existingAsset.width ?? newAsset.width,
height: existingAsset.height ?? newAsset.height,
createdTime: oldestCreationTime,
);
}
@override
String toString() => """
{
"id": "$id",
"id": "${id ?? "-"}",
"remoteId": "${remoteId ?? "-"}",
"localId": "${localId ?? "-"}",
"name": "$name",
@@ -163,8 +172,7 @@ class Asset {
livePhotoVideoId.hashCode;
}
static int compareByRemoteId(Asset a, Asset b) =>
CollectionUtil.compareToNullable(a.remoteId, b.remoteId);
static int compareByHash(Asset a, Asset b) => a.hash.compareTo(b.hash);
static int compareByLocalId(Asset a, Asset b) =>
CollectionUtil.compareToNullable(a.localId, b.localId);
@@ -0,0 +1,56 @@
import 'dart:async';
import 'dart:io';
enum DeviceAssetRequestStatus {
preparing,
downloading,
success,
failed,
}
class DeviceAssetDownloadHandler {
DeviceAssetDownloadHandler() : stream = const Stream.empty() {
assert(
Platform.isIOS || Platform.isMacOS,
'$runtimeType should only be used on iOS or macOS.',
);
}
/// A stream that provides information about the download status and progress of the asset being downloaded.
Stream<DeviceAssetDownloadState> stream;
}
class DeviceAssetDownloadState {
final double progress;
final DeviceAssetRequestStatus status;
const DeviceAssetDownloadState({
required this.progress,
required this.status,
});
DeviceAssetDownloadState copyWith({
double? progress,
DeviceAssetRequestStatus? status,
}) {
return DeviceAssetDownloadState(
progress: progress ?? this.progress,
status: status ?? this.status,
);
}
@override
String toString() {
return 'DeviceAssetDownloadState(progress: $progress, status: $status)';
}
@override
bool operator ==(covariant DeviceAssetDownloadState other) {
return other.progress == progress && other.status == status;
}
@override
int get hashCode {
return progress.hashCode ^ status.hashCode;
}
}
@@ -0,0 +1,57 @@
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/utils/collection_util.dart';
@immutable
class DeviceAssetToHash {
final int? id;
final String localId;
final String hash;
final DateTime modifiedTime;
const DeviceAssetToHash({
this.id,
required this.localId,
required this.hash,
required this.modifiedTime,
});
@override
bool operator ==(covariant DeviceAssetToHash other) {
if (identical(this, other)) return true;
return other.id == id &&
other.localId == localId &&
other.hash == hash &&
other.modifiedTime == modifiedTime;
}
@override
int get hashCode {
return id.hashCode ^
localId.hashCode ^
hash.hashCode ^
modifiedTime.hashCode;
}
DeviceAssetToHash copyWith({
int? id,
String? localId,
String? hash,
DateTime? modifiedTime,
}) {
return DeviceAssetToHash(
id: id ?? this.id,
localId: localId ?? this.localId,
hash: hash ?? this.hash,
modifiedTime: modifiedTime ?? this.modifiedTime,
);
}
@override
String toString() {
return 'DeviceAssetToHash(id: ${id ?? "-"}, localId: $localId, hash: $hash, modifiedTime: $modifiedTime)';
}
static int compareByLocalId(DeviceAssetToHash a, DeviceAssetToHash b) =>
CollectionUtil.compareToNullable(a.localId, b.localId);
}
@@ -16,7 +16,6 @@ extension LevelExtension on Level {
LogLevel toLogLevel() => switch (this) {
Level.FINEST => LogLevel.verbose,
Level.FINE => LogLevel.debug,
Level.INFO => LogLevel.info,
Level.WARNING => LogLevel.warning,
Level.SEVERE => LogLevel.error,
Level.SHOUT => LogLevel.wtf,
+7 -7
View File
@@ -33,35 +33,35 @@ class StoreKeyNotFoundException implements Exception {
/// Key for each possible value in the `Store`.
/// Also stores the converter to convert the value to and from the store and the type of value stored in the Store
enum StoreKey<T, U> {
serverEndpoint<String, String>(
serverEndpoint<String, String>._(
0,
converter: StoreStringConverter(),
type: String,
),
accessToken<String, String>(
accessToken<String, String>._(
1,
converter: StoreStringConverter(),
type: String,
),
currentUser<User, String>(
currentUser<User, String>._(
2,
converter: StoreUserConverter(),
type: String,
),
// App settings
appTheme<AppTheme, int>(
appTheme<AppTheme, int>._(
1000,
converter: StoreEnumConverter(AppTheme.values),
type: int,
),
themeMode<ThemeMode, int>(
themeMode<ThemeMode, int>._(
1001,
converter: StoreEnumConverter(ThemeMode.values),
type: int,
),
darkMode<bool, int>(1002, converter: StoreBooleanConverter(), type: int);
darkMode<bool, int>._(1002, converter: StoreBooleanConverter(), type: int);
const StoreKey(this.id, {required this.converter, required this.type});
const StoreKey._(this.id, {required this.converter, required this.type});
final int id;
/// Primitive Type is also stored here to easily fetch it during runtime