feat: full local assets / album sync
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user