diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 854f852e3c..6fa9a13212 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -55,6 +55,9 @@ custom_lint: restrict: package:photo_manager allowed: # required / wanted + - 'lib/domain/interfaces/album_media.interface.dart' + - 'lib/infrastructure/repositories/album_media.repository.dart' + - 'lib/domain/services/sync.service.dart' - 'lib/repositories/{album,asset,file}_media.repository.dart' # acceptable exceptions for the time being - lib/entities/asset.entity.dart # to provide local AssetEntity for now diff --git a/mobile/drift_schemas/main/drift_schema_v1.json b/mobile/drift_schemas/main/drift_schema_v1.json index 1870ef477f..1508394b75 100644 --- a/mobile/drift_schemas/main/drift_schema_v1.json +++ b/mobile/drift_schemas/main/drift_schema_v1.json @@ -1 +1 @@ -{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"preferences","getter_name":"preferences","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userPreferenceConverter","dart_type_name":"UserPreferences"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}}]} \ No newline at end of file +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"preferences","getter_name":"preferences","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userPreferenceConverter","dart_type_name":"UserPreferences"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}},{"id":3,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"local_id","getter_name":"localId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["local_id"]}},{"id":4,"references":[3],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"asset_count","getter_name":"assetCount","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"thumbnail_id","getter_name":"thumbnailId","moor_type":"string","nullable":true,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (local_id) ON DELETE SET NULL","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (local_id) ON DELETE SET NULL"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_all","getter_name":"isAll","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_all\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_all\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":5,"references":[3,4],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (local_id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (local_id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":6,"references":[3],"type":"index","data":{"on":3,"name":"local_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}}]} \ No newline at end of file diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart index a91e0a715d..0c0eaedb52 100644 --- a/mobile/lib/constants/constants.dart +++ b/mobile/lib/constants/constants.dart @@ -11,3 +11,6 @@ const int kSyncEventBatchSize = 5000; // Hash batch limits const int kBatchHashFileLimit = 128; const int kBatchHashSizeLimit = 1024 * 1024 * 1024; // 1GB + +// Sync related +const int kFetchLocalAssetsBatchSize = 40000; diff --git a/mobile/lib/domain/interfaces/album_media.interface.dart b/mobile/lib/domain/interfaces/album_media.interface.dart new file mode 100644 index 0000000000..4848bce89f --- /dev/null +++ b/mobile/lib/domain/interfaces/album_media.interface.dart @@ -0,0 +1,10 @@ +import 'package:immich_mobile/domain/models/asset/asset.model.dart'; +import 'package:photo_manager/photo_manager.dart'; + +abstract interface class IAlbumMediaRepository { + Future> getAll({PMFilter? filter}); + + Future> getAssetsForAlbum(AssetPathEntity album); + + Future refresh(String albumId, {PMFilter? filter}); +} diff --git a/mobile/lib/domain/interfaces/local_album.interface.dart b/mobile/lib/domain/interfaces/local_album.interface.dart new file mode 100644 index 0000000000..82a79963b2 --- /dev/null +++ b/mobile/lib/domain/interfaces/local_album.interface.dart @@ -0,0 +1,17 @@ +import 'package:immich_mobile/domain/interfaces/db.interface.dart'; +import 'package:immich_mobile/domain/models/local_album.model.dart'; + +abstract interface class ILocalAlbumRepository implements IDatabaseRepository { + Future upsert(LocalAlbum localAlbum); + + Future> getAll({SortLocalAlbumsBy? sortBy}); + + /// Get all asset ids that are only in the album and not in other albums. + /// This is used to determine which assets are unique to the album. + /// This is useful in cases where the album is a smart album or a user-created album, especially in iOS + Future> getAssetIdsOnlyInAlbum(String albumId); + + Future delete(String albumId); +} + +enum SortLocalAlbumsBy { id } diff --git a/mobile/lib/domain/interfaces/local_album_asset.interface.dart b/mobile/lib/domain/interfaces/local_album_asset.interface.dart new file mode 100644 index 0000000000..31f11117ff --- /dev/null +++ b/mobile/lib/domain/interfaces/local_album_asset.interface.dart @@ -0,0 +1,11 @@ +import 'package:immich_mobile/domain/interfaces/db.interface.dart'; +import 'package:immich_mobile/domain/models/asset/asset.model.dart'; + +abstract interface class ILocalAlbumAssetRepository + implements IDatabaseRepository { + Future> getAssetsForAlbum(String albumId); + + Future linkAssetsToAlbum(String albumId, Iterable assetIds); + + Future unlinkAssetsFromAlbum(String albumId, Iterable assetIds); +} diff --git a/mobile/lib/domain/interfaces/local_asset.interface.dart b/mobile/lib/domain/interfaces/local_asset.interface.dart new file mode 100644 index 0000000000..636a49d0e3 --- /dev/null +++ b/mobile/lib/domain/interfaces/local_asset.interface.dart @@ -0,0 +1,10 @@ +import 'package:immich_mobile/domain/interfaces/db.interface.dart'; +import 'package:immich_mobile/domain/models/asset/asset.model.dart'; + +abstract interface class ILocalAssetRepository implements IDatabaseRepository { + Future get(String assetId); + + Future upsertAll(Iterable localAssets); + + Future deleteIds(Iterable ids); +} diff --git a/mobile/lib/domain/models/asset/asset.model.dart b/mobile/lib/domain/models/asset/asset.model.dart new file mode 100644 index 0000000000..e6b11c3130 --- /dev/null +++ b/mobile/lib/domain/models/asset/asset.model.dart @@ -0,0 +1,72 @@ +part 'local_asset.model.dart'; +part 'merged_asset.model.dart'; +part 'remote_asset.model.dart'; + +enum AssetType { + // do not change this order! + other, + image, + video, + audio, +} + +sealed class Asset { + final String name; + final String? checksum; + final AssetType type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + + const Asset({ + required this.name, + required this.checksum, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + }); + + @override + String toString() { + return '''Asset { + name: $name, + type: $type, + createdAt: $createdAt, + updatedAt: $updatedAt, + width: ${width ?? ""}, + height: ${height ?? ""}, + durationInSeconds: ${durationInSeconds ?? ""} +}'''; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is Asset) { + return name == other.name && + type == other.type && + createdAt == other.createdAt && + updatedAt == other.updatedAt && + width == other.width && + height == other.height && + durationInSeconds == other.durationInSeconds; + } + return false; + } + + @override + int get hashCode { + return name.hashCode ^ + type.hashCode ^ + createdAt.hashCode ^ + updatedAt.hashCode ^ + width.hashCode ^ + height.hashCode ^ + durationInSeconds.hashCode; + } +} diff --git a/mobile/lib/domain/models/asset/local_asset.model.dart b/mobile/lib/domain/models/asset/local_asset.model.dart new file mode 100644 index 0000000000..9e4f655dff --- /dev/null +++ b/mobile/lib/domain/models/asset/local_asset.model.dart @@ -0,0 +1,66 @@ +part of 'asset.model.dart'; + +class LocalAsset extends Asset { + final String localId; + + const LocalAsset({ + required this.localId, + required super.name, + super.checksum, + required super.type, + required super.createdAt, + required super.updatedAt, + super.width, + super.height, + super.durationInSeconds, + }); + + @override + String toString() { + return '''LocalAsset { + localId: $localId, + name: $name, + type: $type, + createdAt: $createdAt, + updatedAt: $updatedAt, + width: ${width ?? ""}, + height: ${height ?? ""}, + durationInSeconds: ${durationInSeconds ?? ""} + }'''; + } + + @override + bool operator ==(covariant LocalAsset other) { + if (identical(this, other)) return true; + return super == other && localId == other.localId; + } + + @override + int get hashCode { + return super.hashCode ^ localId.hashCode; + } + + LocalAsset copyWith({ + String? localId, + String? name, + String? checksum, + AssetType? type, + DateTime? createdAt, + DateTime? updatedAt, + int? width, + int? height, + int? durationInSeconds, + }) { + return LocalAsset( + localId: localId ?? this.localId, + name: name ?? this.name, + checksum: checksum ?? this.checksum, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + ); + } +} diff --git a/mobile/lib/domain/models/asset/merged_asset.model.dart b/mobile/lib/domain/models/asset/merged_asset.model.dart new file mode 100644 index 0000000000..afb656ba82 --- /dev/null +++ b/mobile/lib/domain/models/asset/merged_asset.model.dart @@ -0,0 +1,47 @@ +part of 'asset.model.dart'; + +class MergedAsset extends Asset { + final String remoteId; + final String localId; + + const MergedAsset({ + required this.remoteId, + required this.localId, + required super.name, + required super.checksum, + required super.type, + required super.createdAt, + required super.updatedAt, + super.width, + super.height, + super.durationInSeconds, + }); + + @override + String toString() { + return '''MergedAsset { + remoteId: $remoteId, + localId: $localId, + name: $name, + type: $type, + createdAt: $createdAt, + updatedAt: $updatedAt, + width: ${width ?? ""}, + height: ${height ?? ""}, + durationInSeconds: ${durationInSeconds ?? ""} + }'''; + } + + @override + bool operator ==(covariant MergedAsset other) { + if (identical(this, other)) return true; + return super == other && + remoteId == other.remoteId && + localId == other.localId; + } + + @override + int get hashCode { + return super.hashCode ^ remoteId.hashCode ^ localId.hashCode; + } +} diff --git a/mobile/lib/domain/models/asset/remote_asset.model.dart b/mobile/lib/domain/models/asset/remote_asset.model.dart new file mode 100644 index 0000000000..279baaa867 --- /dev/null +++ b/mobile/lib/domain/models/asset/remote_asset.model.dart @@ -0,0 +1,42 @@ +part of 'asset.model.dart'; + +class RemoteAsset extends Asset { + final String remoteId; + + const RemoteAsset({ + required this.remoteId, + required super.name, + required super.checksum, + required super.type, + required super.createdAt, + required super.updatedAt, + super.width, + super.height, + super.durationInSeconds, + }); + + @override + String toString() { + return '''RemoteAsset { + remoteId: $remoteId, + name: $name, + type: $type, + createdAt: $createdAt, + updatedAt: $updatedAt, + width: ${width ?? ""}, + height: ${height ?? ""}, + durationInSeconds: ${durationInSeconds ?? ""} + }'''; + } + + @override + bool operator ==(covariant RemoteAsset other) { + if (identical(this, other)) return true; + return super == other && remoteId == other.remoteId; + } + + @override + int get hashCode { + return super.hashCode ^ remoteId.hashCode; + } +} diff --git a/mobile/lib/domain/models/local_album.model.dart b/mobile/lib/domain/models/local_album.model.dart new file mode 100644 index 0000000000..9745e40550 --- /dev/null +++ b/mobile/lib/domain/models/local_album.model.dart @@ -0,0 +1,84 @@ +enum BackupSelection { + none, + selected, + excluded, +} + +class LocalAlbum { + final String id; + final String name; + final DateTime updatedAt; + + /// Whether the album contains all photos (i.e, the virtual "Recent" album) + final bool isAll; + final int assetCount; + final String? thumbnailId; + final BackupSelection backupSelection; + + const LocalAlbum({ + required this.id, + required this.name, + required this.updatedAt, + this.assetCount = 0, + this.thumbnailId, + this.backupSelection = BackupSelection.none, + this.isAll = false, + }); + + LocalAlbum copyWith({ + String? id, + String? name, + DateTime? updatedAt, + int? assetCount, + String? thumbnailId, + BackupSelection? backupSelection, + bool? isAll, + }) { + return LocalAlbum( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + assetCount: assetCount ?? this.assetCount, + thumbnailId: thumbnailId ?? this.thumbnailId, + backupSelection: backupSelection ?? this.backupSelection, + isAll: isAll ?? this.isAll, + ); + } + + @override + bool operator ==(covariant LocalAlbum other) { + if (identical(this, other)) return true; + + return other.id == id && + other.name == name && + other.updatedAt == updatedAt && + other.assetCount == assetCount && + other.isAll == isAll && + other.thumbnailId == thumbnailId && + other.backupSelection == backupSelection; + } + + @override + int get hashCode { + return id.hashCode ^ + name.hashCode ^ + updatedAt.hashCode ^ + assetCount.hashCode ^ + isAll.hashCode ^ + thumbnailId.hashCode ^ + backupSelection.hashCode; + } + + @override + String toString() { + return '''LocalAlbum: { +id: $id, +name: $name, +updatedAt: $updatedAt, +assetCount: $assetCount, +thumbnailId: ${thumbnailId ?? ''}, +backupSelection: $backupSelection, +isAll: $isAll +}'''; + } +} diff --git a/mobile/lib/domain/services/sync.service.dart b/mobile/lib/domain/services/sync.service.dart new file mode 100644 index 0000000000..7a059fdbbb --- /dev/null +++ b/mobile/lib/domain/services/sync.service.dart @@ -0,0 +1,357 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:immich_mobile/domain/interfaces/album_media.interface.dart'; +import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; +import 'package:immich_mobile/domain/interfaces/local_album_asset.interface.dart'; +import 'package:immich_mobile/domain/interfaces/local_asset.interface.dart'; +import 'package:immich_mobile/domain/models/asset/asset.model.dart'; +import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/utils/diff.dart'; +import 'package:logging/logging.dart'; +import 'package:photo_manager/photo_manager.dart'; + +class SyncService { + final IAlbumMediaRepository _albumMediaRepository; + final ILocalAlbumRepository _localAlbumRepository; + final ILocalAssetRepository _localAssetRepository; + final ILocalAlbumAssetRepository _localAlbumAssetRepository; + final Logger _log = Logger("SyncService"); + + SyncService({ + required IAlbumMediaRepository albumMediaRepository, + required ILocalAlbumRepository localAlbumRepository, + required ILocalAssetRepository localAssetRepository, + required ILocalAlbumAssetRepository localAlbumAssetRepository, + }) : _albumMediaRepository = albumMediaRepository, + _localAlbumRepository = localAlbumRepository, + _localAssetRepository = localAssetRepository, + _localAlbumAssetRepository = localAlbumAssetRepository; + + late final albumFilter = FilterOptionGroup( + imageOption: const FilterOption( + // needTitle is expected to be slow on iOS but is required to fetch the asset title + needTitle: true, + sizeConstraint: SizeConstraint(ignoreSize: true), + ), + videoOption: const FilterOption( + needTitle: true, + sizeConstraint: SizeConstraint(ignoreSize: true), + durationConstraint: DurationConstraint(allowNullable: true), + ), + // This is needed to get the modified time of the album + containsPathModified: true, + createTimeCond: DateTimeCond.def().copyWith(ignore: true), + updateTimeCond: DateTimeCond.def().copyWith(ignore: true), + orders: const [ + // Always sort the result by createdDate.des to update the thumbnail + OrderOption(type: OrderOptionType.createDate, asc: false), + ], + ); + + Future syncLocalAlbums() async { + try { + final Stopwatch stopwatch = Stopwatch()..start(); + + // Use an AdvancedCustomFilter to get all albums faster + final filter = AdvancedCustomFilter( + orderBy: [OrderByItem.asc(CustomColumns.base.id)], + ); + final deviceAlbums = await _albumMediaRepository.getAll(filter: filter); + final dbAlbums = + await _localAlbumRepository.getAll(sortBy: SortLocalAlbumsBy.id); + + final hasChange = await diffSortedLists( + dbAlbums, + await Future.wait( + deviceAlbums.map((a) => a.toDto(withAssetCount: false)), + ), + compare: (a, b) => a.id.compareTo(b.id), + both: diffLocalAlbums, + onlyFirst: removeLocalAlbum, + onlySecond: addLocalAlbum, + ); + + stopwatch.stop(); + _log.info("Full device sync took - ${stopwatch.elapsedMilliseconds}ms"); + return hasChange; + } catch (e, s) { + _log.severe("Error performing full device sync", e, s); + } + return false; + } + + Future addLocalAlbum(LocalAlbum newAlbum) async { + try { + _log.info("Adding device album ${newAlbum.name}"); + final deviceAlbum = + await _albumMediaRepository.refresh(newAlbum.id, filter: albumFilter); + + final assets = newAlbum.assetCount > 0 + ? (await _albumMediaRepository.getAssetsForAlbum(deviceAlbum)) + : []; + final album = (await deviceAlbum.toDto()).copyWith( + // The below assumes the list is already sorted by createdDate from the filter + thumbnailId: assets.firstOrNull?.localId, + ); + + await _localAlbumRepository.transaction(() async { + if (newAlbum.assetCount > 0) { + await _localAssetRepository.upsertAll(assets); + } + // Needs to be after asset upsert to link the thumbnail + await _localAlbumRepository.upsert(album); + + if (newAlbum.assetCount > 0) { + await _localAlbumAssetRepository.linkAssetsToAlbum( + album.id, + assets.map((a) => a.localId), + ); + } + }); + } catch (e, s) { + _log.warning("Error while adding device album", e, s); + } + } + + Future removeLocalAlbum(LocalAlbum album) async { + _log.info("Removing device album ${album.name}"); + try { + // Remove all assets that are only in this particular album + // We cannot remove all assets in the album because they might be in other albums in iOS + final assetsToDelete = + await _localAlbumRepository.getAssetIdsOnlyInAlbum(album.id); + await _localAlbumRepository.transaction(() async { + if (assetsToDelete.isNotEmpty) { + await _localAssetRepository.deleteIds(assetsToDelete); + } + await _localAlbumRepository.delete(album.id); + }); + } catch (e, s) { + _log.warning("Error while removing device album", e, s); + } + } + + @visibleForTesting + // The deviceAlbum is ignored since we are going to refresh it anyways + FutureOr diffLocalAlbums(LocalAlbum dbAlbum, LocalAlbum _) async { + try { + _log.info("Syncing device album ${dbAlbum.name}"); + + final albumEntity = + await _albumMediaRepository.refresh(dbAlbum.id, filter: albumFilter); + final deviceAlbum = await albumEntity.toDto(); + + // Early return if album hasn't changed + if (deviceAlbum.updatedAt.isAtSameMomentAs(dbAlbum.updatedAt) && + deviceAlbum.assetCount == dbAlbum.assetCount) { + _log.info( + "Device album ${dbAlbum.name} has not changed. Skipping sync.", + ); + return false; + } + + // Skip empty albums that don't need syncing + if (deviceAlbum.assetCount == 0 && dbAlbum.assetCount == 0) { + await _localAlbumRepository.upsert( + deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection), + ); + _log.info("Album ${dbAlbum.name} is empty. Only metadata updated."); + return true; + } + + _log.info("Device album ${dbAlbum.name} has changed. Syncing..."); + + // Handle the case where assets are only added - fast path + if (await handleOnlyAssetsAdded(dbAlbum, deviceAlbum)) { + _log.info("Fast synced device album ${dbAlbum.name}"); + return true; + } + + // Slower path - full sync + return await handleAssetUpdate(dbAlbum, deviceAlbum, albumEntity); + } catch (e, s) { + _log.warning("Error while diff device album", e, s); + } + return true; + } + + @visibleForTesting + Future handleOnlyAssetsAdded( + LocalAlbum dbAlbum, + LocalAlbum deviceAlbum, + ) async { + try { + _log.info("Fast syncing device album ${dbAlbum.name}"); + if (!deviceAlbum.updatedAt.isAfter(dbAlbum.updatedAt)) { + _log.info( + "Local album ${deviceAlbum.name} has modifications. Proceeding to full sync", + ); + return false; + } + + // Assets has been modified + if (deviceAlbum.assetCount <= dbAlbum.assetCount) { + _log.info("Local album has modifications. Proceeding to full sync"); + return false; + } + + // Get all assets that are modified after the last known modifiedTime + final filter = albumFilter.copyWith( + updateTimeCond: DateTimeCond( + min: dbAlbum.updatedAt.add(const Duration(seconds: 1)), + max: deviceAlbum.updatedAt, + ), + ); + final modifiedAlbum = + await _albumMediaRepository.refresh(deviceAlbum.id, filter: filter); + final newAssets = + await _albumMediaRepository.getAssetsForAlbum(modifiedAlbum); + + // Early return if no new assets were found + if (newAssets.isEmpty) { + _log.info( + "No new assets found despite album changes. Proceeding to full sync for ${dbAlbum.name}", + ); + return false; + } + + // Check whether there is only addition or if there has been deletions + if (deviceAlbum.assetCount != dbAlbum.assetCount + newAssets.length) { + _log.info("Local album has modifications. Proceeding to full sync"); + return false; + } + + String? thumbnailId = dbAlbum.thumbnailId; + if (thumbnailId == null || newAssets.isNotEmpty) { + if (thumbnailId == null) { + thumbnailId = newAssets.firstOrNull?.localId; + } else if (newAssets.isNotEmpty) { + // The below assumes the list is already sorted by createdDate from the filter + final oldThumbAsset = await _localAssetRepository.get(thumbnailId); + if (oldThumbAsset.createdAt + .isBefore(newAssets.firstOrNull!.createdAt)) { + thumbnailId = newAssets.firstOrNull?.localId; + } + } + } + + await _localAlbumRepository.transaction(() async { + await _localAssetRepository.upsertAll(newAssets); + await _localAlbumAssetRepository.linkAssetsToAlbum( + deviceAlbum.id, + newAssets.map(((a) => a.localId)), + ); + await _localAlbumRepository.upsert( + deviceAlbum.copyWith( + thumbnailId: thumbnailId, + backupSelection: dbAlbum.backupSelection, + ), + ); + }); + + return true; + } catch (e, s) { + _log.warning("Error on fast syncing local album: ${dbAlbum.name}", e, s); + } + return false; + } + + @visibleForTesting + Future handleAssetUpdate( + LocalAlbum dbAlbum, + LocalAlbum deviceAlbum, + AssetPathEntity deviceAlbumEntity, + ) async { + try { + final assetsInDevice = deviceAlbum.assetCount > 0 + ? await _albumMediaRepository.getAssetsForAlbum(deviceAlbumEntity) + : []; + + final assetsInDb = dbAlbum.assetCount > 0 + ? await _localAlbumAssetRepository.getAssetsForAlbum(dbAlbum.id) + : []; + + // The below assumes the list is already sorted by createdDate from the filter + String? thumbnailId = + assetsInDevice.firstOrNull?.localId ?? dbAlbum.thumbnailId; + + final assetsToAdd = {}, + assetsToUpsert = {}, + assetsToDelete = {}; + if (deviceAlbum.assetCount == 0) { + assetsToDelete.addAll(assetsInDb.map((asset) => asset.localId)); + thumbnailId = null; + } else if (dbAlbum.assetCount == 0) { + assetsToAdd.addAll(assetsInDevice); + } else { + assetsInDb.sort((a, b) => a.localId.compareTo(b.localId)); + assetsInDevice.sort((a, b) => a.localId.compareTo(b.localId)); + diffSortedListsSync( + assetsInDb, + assetsInDevice, + compare: (a, b) => a.localId.compareTo(b.localId), + both: (dbAsset, deviceAsset) { + if (dbAsset == deviceAsset) { + return false; + } + assetsToUpsert.add(deviceAsset); + return true; + }, + onlyFirst: (dbAsset) => assetsToDelete.add(dbAsset.localId), + onlySecond: (deviceAsset) => assetsToAdd.add(deviceAsset), + ); + } + _log.info( + "Syncing ${deviceAlbum.name}. ${assetsToAdd.length} assets to add, ${assetsToUpsert.length} assets to update and ${assetsToDelete.length} assets to delete", + ); + + // Populate the album meta + final updatedAlbum = deviceAlbum.copyWith( + thumbnailId: thumbnailId, + backupSelection: dbAlbum.backupSelection, + ); + + // Only query for assets unique to album if we have assets to delete + final assetsOnlyInAlbum = assetsToDelete.isEmpty + ? {} + : (await _localAlbumRepository.getAssetIdsOnlyInAlbum(deviceAlbum.id)) + .toSet(); + + await _localAlbumRepository.transaction(() async { + await _localAssetRepository + .upsertAll(assetsToAdd.followedBy(assetsToUpsert)); + await _localAlbumAssetRepository.linkAssetsToAlbum( + dbAlbum.id, + assetsToAdd.map((a) => a.localId), + ); + await _localAlbumRepository.upsert(updatedAlbum); + // Remove all assets that are only in this particular album + // We cannot remove all assets in the album because they might be in other albums in iOS + await _localAssetRepository.deleteIds( + assetsToDelete.intersection(assetsOnlyInAlbum), + ); + // Unlink the others + await _localAlbumAssetRepository.unlinkAssetsFromAlbum( + dbAlbum.id, + assetsToDelete.difference(assetsOnlyInAlbum), + ); + }); + } catch (e, s) { + _log.warning("Error on full syncing local album: ${dbAlbum.name}", e, s); + } + return true; + } +} + +extension AssetPathEntitySyncX on AssetPathEntity { + Future toDto({bool withAssetCount = true}) async => LocalAlbum( + id: id, + name: name, + updatedAt: lastModified ?? DateTime.now(), + // the assetCountAsync call is expensive for larger albums with several thousand assets + assetCount: withAssetCount ? await assetCountAsync : 0, + backupSelection: BackupSelection.none, + isAll: isAll, + ); +} diff --git a/mobile/lib/infrastructure/entities/local_album.entity.dart b/mobile/lib/infrastructure/entities/local_album.entity.dart new file mode 100644 index 0000000000..e4307ef410 --- /dev/null +++ b/mobile/lib/infrastructure/entities/local_album.entity.dart @@ -0,0 +1,35 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class LocalAlbumEntity extends Table with DriftDefaultsMixin { + const LocalAlbumEntity(); + + TextColumn get id => text()(); + TextColumn get name => text()(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + IntColumn get assetCount => integer().withDefault(const Constant(0))(); + TextColumn get thumbnailId => text() + .nullable() + .references(LocalAssetEntity, #localId, onDelete: KeyAction.setNull)(); + IntColumn get backupSelection => intEnum()(); + BoolColumn get isAll => boolean().withDefault(const Constant(false))(); + + @override + Set get primaryKey => {id}; +} + +extension LocalAlbumEntityX on LocalAlbumEntityData { + LocalAlbum toDto() { + return LocalAlbum( + id: id, + name: name, + updatedAt: updatedAt, + thumbnailId: thumbnailId, + backupSelection: backupSelection, + isAll: isAll, + ); + } +} diff --git a/mobile/lib/infrastructure/entities/local_album.entity.drift.dart b/mobile/lib/infrastructure/entities/local_album.entity.drift.dart new file mode 100644 index 0000000000..19ada47e87 --- /dev/null +++ b/mobile/lib/infrastructure/entities/local_album.entity.drift.dart @@ -0,0 +1,732 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' + as i1; +import 'package:immich_mobile/domain/models/local_album.model.dart' as i2; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart' + as i3; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' + as i5; +import 'package:drift/internal/modular.dart' as i6; + +typedef $$LocalAlbumEntityTableCreateCompanionBuilder + = i1.LocalAlbumEntityCompanion Function({ + required String id, + required String name, + i0.Value updatedAt, + i0.Value assetCount, + i0.Value thumbnailId, + required i2.BackupSelection backupSelection, + i0.Value isAll, +}); +typedef $$LocalAlbumEntityTableUpdateCompanionBuilder + = i1.LocalAlbumEntityCompanion Function({ + i0.Value id, + i0.Value name, + i0.Value updatedAt, + i0.Value assetCount, + i0.Value thumbnailId, + i0.Value backupSelection, + i0.Value isAll, +}); + +final class $$LocalAlbumEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, i1.$LocalAlbumEntityTable, i1.LocalAlbumEntityData> { + $$LocalAlbumEntityTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static i5.$LocalAssetEntityTable _thumbnailIdTable(i0.GeneratedDatabase db) => + i6.ReadDatabaseContainer(db) + .resultSet('local_asset_entity') + .createAlias(i0.$_aliasNameGenerator( + i6.ReadDatabaseContainer(db) + .resultSet('local_album_entity') + .thumbnailId, + i6.ReadDatabaseContainer(db) + .resultSet('local_asset_entity') + .localId)); + + i5.$$LocalAssetEntityTableProcessedTableManager? get thumbnailId { + final $_column = $_itemColumn('thumbnail_id'); + if ($_column == null) return null; + final manager = i5 + .$$LocalAssetEntityTableTableManager( + $_db, + i6.ReadDatabaseContainer($_db) + .resultSet('local_asset_entity')) + .filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_thumbnailIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$LocalAlbumEntityTableFilterComposer + extends i0.Composer { + $$LocalAlbumEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get assetCount => $composableBuilder( + column: $table.assetCount, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnWithTypeConverterFilters + get backupSelection => $composableBuilder( + column: $table.backupSelection, + builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + + i0.ColumnFilters get isAll => $composableBuilder( + column: $table.isAll, builder: (column) => i0.ColumnFilters(column)); + + i5.$$LocalAssetEntityTableFilterComposer get thumbnailId { + final i5.$$LocalAssetEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.thumbnailId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('local_asset_entity'), + getReferencedColumn: (t) => t.localId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$LocalAssetEntityTableFilterComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('local_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$LocalAlbumEntityTableOrderingComposer + extends i0.Composer { + $$LocalAlbumEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get assetCount => $composableBuilder( + column: $table.assetCount, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get backupSelection => $composableBuilder( + column: $table.backupSelection, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get isAll => $composableBuilder( + column: $table.isAll, builder: (column) => i0.ColumnOrderings(column)); + + i5.$$LocalAssetEntityTableOrderingComposer get thumbnailId { + final i5.$$LocalAssetEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.thumbnailId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('local_asset_entity'), + getReferencedColumn: (t) => t.localId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$LocalAssetEntityTableOrderingComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet( + 'local_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$LocalAlbumEntityTableAnnotationComposer + extends i0.Composer { + $$LocalAlbumEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + i0.GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + i0.GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + i0.GeneratedColumn get assetCount => $composableBuilder( + column: $table.assetCount, builder: (column) => column); + + i0.GeneratedColumnWithTypeConverter + get backupSelection => $composableBuilder( + column: $table.backupSelection, builder: (column) => column); + + i0.GeneratedColumn get isAll => + $composableBuilder(column: $table.isAll, builder: (column) => column); + + i5.$$LocalAssetEntityTableAnnotationComposer get thumbnailId { + final i5.$$LocalAssetEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.thumbnailId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('local_asset_entity'), + getReferencedColumn: (t) => t.localId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$LocalAssetEntityTableAnnotationComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet( + 'local_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$LocalAlbumEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$LocalAlbumEntityTable, + i1.LocalAlbumEntityData, + i1.$$LocalAlbumEntityTableFilterComposer, + i1.$$LocalAlbumEntityTableOrderingComposer, + i1.$$LocalAlbumEntityTableAnnotationComposer, + $$LocalAlbumEntityTableCreateCompanionBuilder, + $$LocalAlbumEntityTableUpdateCompanionBuilder, + (i1.LocalAlbumEntityData, i1.$$LocalAlbumEntityTableReferences), + i1.LocalAlbumEntityData, + i0.PrefetchHooks Function({bool thumbnailId})> { + $$LocalAlbumEntityTableTableManager( + i0.GeneratedDatabase db, i1.$LocalAlbumEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$LocalAlbumEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => i1 + .$$LocalAlbumEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$LocalAlbumEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value id = const i0.Value.absent(), + i0.Value name = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value assetCount = const i0.Value.absent(), + i0.Value thumbnailId = const i0.Value.absent(), + i0.Value backupSelection = + const i0.Value.absent(), + i0.Value isAll = const i0.Value.absent(), + }) => + i1.LocalAlbumEntityCompanion( + id: id, + name: name, + updatedAt: updatedAt, + assetCount: assetCount, + thumbnailId: thumbnailId, + backupSelection: backupSelection, + isAll: isAll, + ), + createCompanionCallback: ({ + required String id, + required String name, + i0.Value updatedAt = const i0.Value.absent(), + i0.Value assetCount = const i0.Value.absent(), + i0.Value thumbnailId = const i0.Value.absent(), + required i2.BackupSelection backupSelection, + i0.Value isAll = const i0.Value.absent(), + }) => + i1.LocalAlbumEntityCompanion.insert( + id: id, + name: name, + updatedAt: updatedAt, + assetCount: assetCount, + thumbnailId: thumbnailId, + backupSelection: backupSelection, + isAll: isAll, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$LocalAlbumEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({thumbnailId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (thumbnailId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.thumbnailId, + referencedTable: i1.$$LocalAlbumEntityTableReferences + ._thumbnailIdTable(db), + referencedColumn: i1.$$LocalAlbumEntityTableReferences + ._thumbnailIdTable(db) + .localId, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$LocalAlbumEntityTableProcessedTableManager = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$LocalAlbumEntityTable, + i1.LocalAlbumEntityData, + i1.$$LocalAlbumEntityTableFilterComposer, + i1.$$LocalAlbumEntityTableOrderingComposer, + i1.$$LocalAlbumEntityTableAnnotationComposer, + $$LocalAlbumEntityTableCreateCompanionBuilder, + $$LocalAlbumEntityTableUpdateCompanionBuilder, + (i1.LocalAlbumEntityData, i1.$$LocalAlbumEntityTableReferences), + i1.LocalAlbumEntityData, + i0.PrefetchHooks Function({bool thumbnailId})>; + +class $LocalAlbumEntityTable extends i3.LocalAlbumEntity + with i0.TableInfo<$LocalAlbumEntityTable, i1.LocalAlbumEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $LocalAlbumEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); + @override + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _nameMeta = + const i0.VerificationMeta('name'); + @override + late final i0.GeneratedColumn name = i0.GeneratedColumn( + 'name', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _updatedAtMeta = + const i0.VerificationMeta('updatedAt'); + @override + late final i0.GeneratedColumn updatedAt = + i0.GeneratedColumn('updated_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _assetCountMeta = + const i0.VerificationMeta('assetCount'); + @override + late final i0.GeneratedColumn assetCount = i0.GeneratedColumn( + 'asset_count', aliasedName, false, + type: i0.DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const i4.Constant(0)); + static const i0.VerificationMeta _thumbnailIdMeta = + const i0.VerificationMeta('thumbnailId'); + @override + late final i0.GeneratedColumn thumbnailId = + i0.GeneratedColumn('thumbnail_id', aliasedName, true, + type: i0.DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES local_asset_entity (local_id) ON DELETE SET NULL')); + @override + late final i0.GeneratedColumnWithTypeConverter + backupSelection = i0.GeneratedColumn( + 'backup_selection', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true) + .withConverter( + i1.$LocalAlbumEntityTable.$converterbackupSelection); + static const i0.VerificationMeta _isAllMeta = + const i0.VerificationMeta('isAll'); + @override + late final i0.GeneratedColumn isAll = i0.GeneratedColumn( + 'is_all', aliasedName, false, + type: i0.DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: + i0.GeneratedColumn.constraintIsAlways('CHECK ("is_all" IN (0, 1))'), + defaultValue: const i4.Constant(false)); + @override + List get $columns => + [id, name, updatedAt, assetCount, thumbnailId, backupSelection, isAll]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + } + if (data.containsKey('asset_count')) { + context.handle( + _assetCountMeta, + assetCount.isAcceptableOrUnknown( + data['asset_count']!, _assetCountMeta)); + } + if (data.containsKey('thumbnail_id')) { + context.handle( + _thumbnailIdMeta, + thumbnailId.isAcceptableOrUnknown( + data['thumbnail_id']!, _thumbnailIdMeta)); + } + if (data.containsKey('is_all')) { + context.handle( + _isAllMeta, isAll.isAcceptableOrUnknown(data['is_all']!, _isAllMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.LocalAlbumEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.LocalAlbumEntityData( + id: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!, + updatedAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + assetCount: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}asset_count'])!, + thumbnailId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}thumbnail_id']), + backupSelection: i1.$LocalAlbumEntityTable.$converterbackupSelection + .fromSql(attachedDatabase.typeMapping.read(i0.DriftSqlType.int, + data['${effectivePrefix}backup_selection'])!), + isAll: attachedDatabase.typeMapping + .read(i0.DriftSqlType.bool, data['${effectivePrefix}is_all'])!, + ); + } + + @override + $LocalAlbumEntityTable createAlias(String alias) { + return $LocalAlbumEntityTable(attachedDatabase, alias); + } + + static i0.JsonTypeConverter2 + $converterbackupSelection = + const i0.EnumIndexConverter( + i2.BackupSelection.values); + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumEntityData extends i0.DataClass + implements i0.Insertable { + final String id; + final String name; + final DateTime updatedAt; + final int assetCount; + final String? thumbnailId; + final i2.BackupSelection backupSelection; + final bool isAll; + const LocalAlbumEntityData( + {required this.id, + required this.name, + required this.updatedAt, + required this.assetCount, + this.thumbnailId, + required this.backupSelection, + required this.isAll}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = i0.Variable(id); + map['name'] = i0.Variable(name); + map['updated_at'] = i0.Variable(updatedAt); + map['asset_count'] = i0.Variable(assetCount); + if (!nullToAbsent || thumbnailId != null) { + map['thumbnail_id'] = i0.Variable(thumbnailId); + } + { + map['backup_selection'] = i0.Variable(i1 + .$LocalAlbumEntityTable.$converterbackupSelection + .toSql(backupSelection)); + } + map['is_all'] = i0.Variable(isAll); + return map; + } + + factory LocalAlbumEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return LocalAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + updatedAt: serializer.fromJson(json['updatedAt']), + assetCount: serializer.fromJson(json['assetCount']), + thumbnailId: serializer.fromJson(json['thumbnailId']), + backupSelection: i1.$LocalAlbumEntityTable.$converterbackupSelection + .fromJson(serializer.fromJson(json['backupSelection'])), + isAll: serializer.fromJson(json['isAll']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'updatedAt': serializer.toJson(updatedAt), + 'assetCount': serializer.toJson(assetCount), + 'thumbnailId': serializer.toJson(thumbnailId), + 'backupSelection': serializer.toJson(i1 + .$LocalAlbumEntityTable.$converterbackupSelection + .toJson(backupSelection)), + 'isAll': serializer.toJson(isAll), + }; + } + + i1.LocalAlbumEntityData copyWith( + {String? id, + String? name, + DateTime? updatedAt, + int? assetCount, + i0.Value thumbnailId = const i0.Value.absent(), + i2.BackupSelection? backupSelection, + bool? isAll}) => + i1.LocalAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + assetCount: assetCount ?? this.assetCount, + thumbnailId: thumbnailId.present ? thumbnailId.value : this.thumbnailId, + backupSelection: backupSelection ?? this.backupSelection, + isAll: isAll ?? this.isAll, + ); + LocalAlbumEntityData copyWithCompanion(i1.LocalAlbumEntityCompanion data) { + return LocalAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + assetCount: + data.assetCount.present ? data.assetCount.value : this.assetCount, + thumbnailId: + data.thumbnailId.present ? data.thumbnailId.value : this.thumbnailId, + backupSelection: data.backupSelection.present + ? data.backupSelection.value + : this.backupSelection, + isAll: data.isAll.present ? data.isAll.value : this.isAll, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('assetCount: $assetCount, ') + ..write('thumbnailId: $thumbnailId, ') + ..write('backupSelection: $backupSelection, ') + ..write('isAll: $isAll') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, name, updatedAt, assetCount, thumbnailId, backupSelection, isAll); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.LocalAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.updatedAt == this.updatedAt && + other.assetCount == this.assetCount && + other.thumbnailId == this.thumbnailId && + other.backupSelection == this.backupSelection && + other.isAll == this.isAll); +} + +class LocalAlbumEntityCompanion + extends i0.UpdateCompanion { + final i0.Value id; + final i0.Value name; + final i0.Value updatedAt; + final i0.Value assetCount; + final i0.Value thumbnailId; + final i0.Value backupSelection; + final i0.Value isAll; + const LocalAlbumEntityCompanion({ + this.id = const i0.Value.absent(), + this.name = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.assetCount = const i0.Value.absent(), + this.thumbnailId = const i0.Value.absent(), + this.backupSelection = const i0.Value.absent(), + this.isAll = const i0.Value.absent(), + }); + LocalAlbumEntityCompanion.insert({ + required String id, + required String name, + this.updatedAt = const i0.Value.absent(), + this.assetCount = const i0.Value.absent(), + this.thumbnailId = const i0.Value.absent(), + required i2.BackupSelection backupSelection, + this.isAll = const i0.Value.absent(), + }) : id = i0.Value(id), + name = i0.Value(name), + backupSelection = i0.Value(backupSelection); + static i0.Insertable custom({ + i0.Expression? id, + i0.Expression? name, + i0.Expression? updatedAt, + i0.Expression? assetCount, + i0.Expression? thumbnailId, + i0.Expression? backupSelection, + i0.Expression? isAll, + }) { + return i0.RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (updatedAt != null) 'updated_at': updatedAt, + if (assetCount != null) 'asset_count': assetCount, + if (thumbnailId != null) 'thumbnail_id': thumbnailId, + if (backupSelection != null) 'backup_selection': backupSelection, + if (isAll != null) 'is_all': isAll, + }); + } + + i1.LocalAlbumEntityCompanion copyWith( + {i0.Value? id, + i0.Value? name, + i0.Value? updatedAt, + i0.Value? assetCount, + i0.Value? thumbnailId, + i0.Value? backupSelection, + i0.Value? isAll}) { + return i1.LocalAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + updatedAt: updatedAt ?? this.updatedAt, + assetCount: assetCount ?? this.assetCount, + thumbnailId: thumbnailId ?? this.thumbnailId, + backupSelection: backupSelection ?? this.backupSelection, + isAll: isAll ?? this.isAll, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (name.present) { + map['name'] = i0.Variable(name.value); + } + if (updatedAt.present) { + map['updated_at'] = i0.Variable(updatedAt.value); + } + if (assetCount.present) { + map['asset_count'] = i0.Variable(assetCount.value); + } + if (thumbnailId.present) { + map['thumbnail_id'] = i0.Variable(thumbnailId.value); + } + if (backupSelection.present) { + map['backup_selection'] = i0.Variable(i1 + .$LocalAlbumEntityTable.$converterbackupSelection + .toSql(backupSelection.value)); + } + if (isAll.present) { + map['is_all'] = i0.Variable(isAll.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('updatedAt: $updatedAt, ') + ..write('assetCount: $assetCount, ') + ..write('thumbnailId: $thumbnailId, ') + ..write('backupSelection: $backupSelection, ') + ..write('isAll: $isAll') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/local_album_asset.entity.dart b/mobile/lib/infrastructure/entities/local_album_asset.entity.dart new file mode 100644 index 0000000000..f1c3582b5a --- /dev/null +++ b/mobile/lib/infrastructure/entities/local_album_asset.entity.dart @@ -0,0 +1,17 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class LocalAlbumAssetEntity extends Table with DriftDefaultsMixin { + const LocalAlbumAssetEntity(); + + TextColumn get assetId => text() + .references(LocalAssetEntity, #localId, onDelete: KeyAction.cascade)(); + + TextColumn get albumId => + text().references(LocalAlbumEntity, #id, onDelete: KeyAction.cascade)(); + + @override + Set get primaryKey => {assetId, albumId}; +} diff --git a/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart new file mode 100644 index 0000000000..86264ef75e --- /dev/null +++ b/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart @@ -0,0 +1,565 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart' + as i1; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart' + as i2; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' + as i3; +import 'package:drift/internal/modular.dart' as i4; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' + as i5; + +typedef $$LocalAlbumAssetEntityTableCreateCompanionBuilder + = i1.LocalAlbumAssetEntityCompanion Function({ + required String assetId, + required String albumId, +}); +typedef $$LocalAlbumAssetEntityTableUpdateCompanionBuilder + = i1.LocalAlbumAssetEntityCompanion Function({ + i0.Value assetId, + i0.Value albumId, +}); + +final class $$LocalAlbumAssetEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, + i1.$LocalAlbumAssetEntityTable, + i1.LocalAlbumAssetEntityData> { + $$LocalAlbumAssetEntityTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static i3.$LocalAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) => + i4.ReadDatabaseContainer(db) + .resultSet('local_asset_entity') + .createAlias(i0.$_aliasNameGenerator( + i4.ReadDatabaseContainer(db) + .resultSet( + 'local_album_asset_entity') + .assetId, + i4.ReadDatabaseContainer(db) + .resultSet('local_asset_entity') + .localId)); + + i3.$$LocalAssetEntityTableProcessedTableManager get assetId { + final $_column = $_itemColumn('asset_id')!; + + final manager = i3 + .$$LocalAssetEntityTableTableManager( + $_db, + i4.ReadDatabaseContainer($_db) + .resultSet('local_asset_entity')) + .filter((f) => f.localId.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_assetIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static i5.$LocalAlbumEntityTable _albumIdTable(i0.GeneratedDatabase db) => + i4.ReadDatabaseContainer(db) + .resultSet('local_album_entity') + .createAlias(i0.$_aliasNameGenerator( + i4.ReadDatabaseContainer(db) + .resultSet( + 'local_album_asset_entity') + .albumId, + i4.ReadDatabaseContainer(db) + .resultSet('local_album_entity') + .id)); + + i5.$$LocalAlbumEntityTableProcessedTableManager get albumId { + final $_column = $_itemColumn('album_id')!; + + final manager = i5 + .$$LocalAlbumEntityTableTableManager( + $_db, + i4.ReadDatabaseContainer($_db) + .resultSet('local_album_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_albumIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$LocalAlbumAssetEntityTableFilterComposer + extends i0.Composer { + $$LocalAlbumAssetEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i3.$$LocalAssetEntityTableFilterComposer get assetId { + final i3.$$LocalAssetEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('local_asset_entity'), + getReferencedColumn: (t) => t.localId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$LocalAssetEntityTableFilterComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('local_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$LocalAlbumEntityTableFilterComposer get albumId { + final i5.$$LocalAlbumEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.albumId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('local_album_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$LocalAlbumEntityTableFilterComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('local_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$LocalAlbumAssetEntityTableOrderingComposer + extends i0.Composer { + $$LocalAlbumAssetEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i3.$$LocalAssetEntityTableOrderingComposer get assetId { + final i3.$$LocalAssetEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('local_asset_entity'), + getReferencedColumn: (t) => t.localId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$LocalAssetEntityTableOrderingComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'local_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$LocalAlbumEntityTableOrderingComposer get albumId { + final i5.$$LocalAlbumEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.albumId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('local_album_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$LocalAlbumEntityTableOrderingComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'local_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$LocalAlbumAssetEntityTableAnnotationComposer + extends i0.Composer { + $$LocalAlbumAssetEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i3.$$LocalAssetEntityTableAnnotationComposer get assetId { + final i3.$$LocalAssetEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('local_asset_entity'), + getReferencedColumn: (t) => t.localId, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$LocalAssetEntityTableAnnotationComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'local_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$LocalAlbumEntityTableAnnotationComposer get albumId { + final i5.$$LocalAlbumEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.albumId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('local_album_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$LocalAlbumEntityTableAnnotationComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'local_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$LocalAlbumAssetEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$LocalAlbumAssetEntityTable, + i1.LocalAlbumAssetEntityData, + i1.$$LocalAlbumAssetEntityTableFilterComposer, + i1.$$LocalAlbumAssetEntityTableOrderingComposer, + i1.$$LocalAlbumAssetEntityTableAnnotationComposer, + $$LocalAlbumAssetEntityTableCreateCompanionBuilder, + $$LocalAlbumAssetEntityTableUpdateCompanionBuilder, + (i1.LocalAlbumAssetEntityData, i1.$$LocalAlbumAssetEntityTableReferences), + i1.LocalAlbumAssetEntityData, + i0.PrefetchHooks Function({bool assetId, bool albumId})> { + $$LocalAlbumAssetEntityTableTableManager( + i0.GeneratedDatabase db, i1.$LocalAlbumAssetEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$LocalAlbumAssetEntityTableFilterComposer( + $db: db, $table: table), + createOrderingComposer: () => + i1.$$LocalAlbumAssetEntityTableOrderingComposer( + $db: db, $table: table), + createComputedFieldComposer: () => + i1.$$LocalAlbumAssetEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value assetId = const i0.Value.absent(), + i0.Value albumId = const i0.Value.absent(), + }) => + i1.LocalAlbumAssetEntityCompanion( + assetId: assetId, + albumId: albumId, + ), + createCompanionCallback: ({ + required String assetId, + required String albumId, + }) => + i1.LocalAlbumAssetEntityCompanion.insert( + assetId: assetId, + albumId: albumId, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$LocalAlbumAssetEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({assetId = false, albumId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (assetId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.assetId, + referencedTable: i1.$$LocalAlbumAssetEntityTableReferences + ._assetIdTable(db), + referencedColumn: i1.$$LocalAlbumAssetEntityTableReferences + ._assetIdTable(db) + .localId, + ) as T; + } + if (albumId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.albumId, + referencedTable: i1.$$LocalAlbumAssetEntityTableReferences + ._albumIdTable(db), + referencedColumn: i1.$$LocalAlbumAssetEntityTableReferences + ._albumIdTable(db) + .id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$LocalAlbumAssetEntityTableProcessedTableManager + = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$LocalAlbumAssetEntityTable, + i1.LocalAlbumAssetEntityData, + i1.$$LocalAlbumAssetEntityTableFilterComposer, + i1.$$LocalAlbumAssetEntityTableOrderingComposer, + i1.$$LocalAlbumAssetEntityTableAnnotationComposer, + $$LocalAlbumAssetEntityTableCreateCompanionBuilder, + $$LocalAlbumAssetEntityTableUpdateCompanionBuilder, + ( + i1.LocalAlbumAssetEntityData, + i1.$$LocalAlbumAssetEntityTableReferences + ), + i1.LocalAlbumAssetEntityData, + i0.PrefetchHooks Function({bool assetId, bool albumId})>; + +class $LocalAlbumAssetEntityTable extends i2.LocalAlbumAssetEntity + with + i0 + .TableInfo<$LocalAlbumAssetEntityTable, i1.LocalAlbumAssetEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $LocalAlbumAssetEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _assetIdMeta = + const i0.VerificationMeta('assetId'); + @override + late final i0.GeneratedColumn assetId = i0.GeneratedColumn( + 'asset_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES local_asset_entity (local_id) ON DELETE CASCADE')); + static const i0.VerificationMeta _albumIdMeta = + const i0.VerificationMeta('albumId'); + @override + late final i0.GeneratedColumn albumId = i0.GeneratedColumn( + 'album_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES local_album_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_album_asset_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('asset_id')) { + context.handle(_assetIdMeta, + assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta)); + } else if (isInserting) { + context.missing(_assetIdMeta); + } + if (data.containsKey('album_id')) { + context.handle(_albumIdMeta, + albumId.isAcceptableOrUnknown(data['album_id']!, _albumIdMeta)); + } else if (isInserting) { + context.missing(_albumIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {assetId, albumId}; + @override + i1.LocalAlbumAssetEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.LocalAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + albumId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}album_id'])!, + ); + } + + @override + $LocalAlbumAssetEntityTable createAlias(String alias) { + return $LocalAlbumAssetEntityTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAlbumAssetEntityData extends i0.DataClass + implements i0.Insertable { + final String assetId; + final String albumId; + const LocalAlbumAssetEntityData( + {required this.assetId, required this.albumId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = i0.Variable(assetId); + map['album_id'] = i0.Variable(albumId); + return map; + } + + factory LocalAlbumAssetEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return LocalAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + i1.LocalAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + i1.LocalAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + LocalAlbumAssetEntityData copyWithCompanion( + i1.LocalAlbumAssetEntityCompanion data) { + return LocalAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.LocalAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class LocalAlbumAssetEntityCompanion + extends i0.UpdateCompanion { + final i0.Value assetId; + final i0.Value albumId; + const LocalAlbumAssetEntityCompanion({ + this.assetId = const i0.Value.absent(), + this.albumId = const i0.Value.absent(), + }); + LocalAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = i0.Value(assetId), + albumId = i0.Value(albumId); + static i0.Insertable custom({ + i0.Expression? assetId, + i0.Expression? albumId, + }) { + return i0.RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + i1.LocalAlbumAssetEntityCompanion copyWith( + {i0.Value? assetId, i0.Value? albumId}) { + return i1.LocalAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = i0.Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = i0.Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.dart b/mobile/lib/infrastructure/entities/local_asset.entity.dart new file mode 100644 index 0000000000..1620e703f4 --- /dev/null +++ b/mobile/lib/infrastructure/entities/local_asset.entity.dart @@ -0,0 +1,33 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/asset/asset.model.dart'; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +@TableIndex(name: 'local_asset_checksum', columns: {#checksum}) +class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin { + const LocalAssetEntity(); + + TextColumn get localId => text()(); + + TextColumn get checksum => text().nullable()(); + + @override + Set get primaryKey => {localId}; +} + +extension LocalAssetEntityX on LocalAssetEntityData { + LocalAsset toDto() { + return LocalAsset( + localId: localId, + name: name, + checksum: checksum, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + width: width, + height: height, + durationInSeconds: durationInSeconds, + ); + } +} diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart new file mode 100644 index 0000000000..e9ab09e8cf --- /dev/null +++ b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart @@ -0,0 +1,705 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' + as i1; +import 'package:immich_mobile/domain/models/asset/asset.model.dart' as i2; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart' + as i3; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; + +typedef $$LocalAssetEntityTableCreateCompanionBuilder + = i1.LocalAssetEntityCompanion Function({ + required String name, + required i2.AssetType type, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value width, + i0.Value height, + i0.Value durationInSeconds, + required String localId, + i0.Value checksum, +}); +typedef $$LocalAssetEntityTableUpdateCompanionBuilder + = i1.LocalAssetEntityCompanion Function({ + i0.Value name, + i0.Value type, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value width, + i0.Value height, + i0.Value durationInSeconds, + i0.Value localId, + i0.Value checksum, +}); + +class $$LocalAssetEntityTableFilterComposer + extends i0.Composer { + $$LocalAssetEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnWithTypeConverterFilters get type => + $composableBuilder( + column: $table.type, + builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + + i0.ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get width => $composableBuilder( + column: $table.width, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get height => $composableBuilder( + column: $table.height, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get durationInSeconds => $composableBuilder( + column: $table.durationInSeconds, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get localId => $composableBuilder( + column: $table.localId, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get checksum => $composableBuilder( + column: $table.checksum, builder: (column) => i0.ColumnFilters(column)); +} + +class $$LocalAssetEntityTableOrderingComposer + extends i0.Composer { + $$LocalAssetEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get type => $composableBuilder( + column: $table.type, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get width => $composableBuilder( + column: $table.width, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get height => $composableBuilder( + column: $table.height, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get durationInSeconds => $composableBuilder( + column: $table.durationInSeconds, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get localId => $composableBuilder( + column: $table.localId, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get checksum => $composableBuilder( + column: $table.checksum, builder: (column) => i0.ColumnOrderings(column)); +} + +class $$LocalAssetEntityTableAnnotationComposer + extends i0.Composer { + $$LocalAssetEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + i0.GeneratedColumnWithTypeConverter get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + i0.GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + i0.GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + i0.GeneratedColumn get width => + $composableBuilder(column: $table.width, builder: (column) => column); + + i0.GeneratedColumn get height => + $composableBuilder(column: $table.height, builder: (column) => column); + + i0.GeneratedColumn get durationInSeconds => $composableBuilder( + column: $table.durationInSeconds, builder: (column) => column); + + i0.GeneratedColumn get localId => + $composableBuilder(column: $table.localId, builder: (column) => column); + + i0.GeneratedColumn get checksum => + $composableBuilder(column: $table.checksum, builder: (column) => column); +} + +class $$LocalAssetEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$LocalAssetEntityTable, + i1.LocalAssetEntityData, + i1.$$LocalAssetEntityTableFilterComposer, + i1.$$LocalAssetEntityTableOrderingComposer, + i1.$$LocalAssetEntityTableAnnotationComposer, + $$LocalAssetEntityTableCreateCompanionBuilder, + $$LocalAssetEntityTableUpdateCompanionBuilder, + ( + i1.LocalAssetEntityData, + i0.BaseReferences + ), + i1.LocalAssetEntityData, + i0.PrefetchHooks Function()> { + $$LocalAssetEntityTableTableManager( + i0.GeneratedDatabase db, i1.$LocalAssetEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$LocalAssetEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => i1 + .$$LocalAssetEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$LocalAssetEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value name = const i0.Value.absent(), + i0.Value type = const i0.Value.absent(), + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value width = const i0.Value.absent(), + i0.Value height = const i0.Value.absent(), + i0.Value durationInSeconds = const i0.Value.absent(), + i0.Value localId = const i0.Value.absent(), + i0.Value checksum = const i0.Value.absent(), + }) => + i1.LocalAssetEntityCompanion( + name: name, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + width: width, + height: height, + durationInSeconds: durationInSeconds, + localId: localId, + checksum: checksum, + ), + createCompanionCallback: ({ + required String name, + required i2.AssetType type, + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value width = const i0.Value.absent(), + i0.Value height = const i0.Value.absent(), + i0.Value durationInSeconds = const i0.Value.absent(), + required String localId, + i0.Value checksum = const i0.Value.absent(), + }) => + i1.LocalAssetEntityCompanion.insert( + name: name, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + width: width, + height: height, + durationInSeconds: durationInSeconds, + localId: localId, + checksum: checksum, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$LocalAssetEntityTableProcessedTableManager = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$LocalAssetEntityTable, + i1.LocalAssetEntityData, + i1.$$LocalAssetEntityTableFilterComposer, + i1.$$LocalAssetEntityTableOrderingComposer, + i1.$$LocalAssetEntityTableAnnotationComposer, + $$LocalAssetEntityTableCreateCompanionBuilder, + $$LocalAssetEntityTableUpdateCompanionBuilder, + ( + i1.LocalAssetEntityData, + i0.BaseReferences + ), + i1.LocalAssetEntityData, + i0.PrefetchHooks Function()>; +i0.Index get localAssetChecksum => i0.Index('local_asset_checksum', + 'CREATE INDEX local_asset_checksum ON local_asset_entity (checksum)'); + +class $LocalAssetEntityTable extends i3.LocalAssetEntity + with i0.TableInfo<$LocalAssetEntityTable, i1.LocalAssetEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $LocalAssetEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _nameMeta = + const i0.VerificationMeta('name'); + @override + late final i0.GeneratedColumn name = i0.GeneratedColumn( + 'name', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + @override + late final i0.GeneratedColumnWithTypeConverter type = + i0.GeneratedColumn('type', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true) + .withConverter( + i1.$LocalAssetEntityTable.$convertertype); + static const i0.VerificationMeta _createdAtMeta = + const i0.VerificationMeta('createdAt'); + @override + late final i0.GeneratedColumn createdAt = + i0.GeneratedColumn('created_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _updatedAtMeta = + const i0.VerificationMeta('updatedAt'); + @override + late final i0.GeneratedColumn updatedAt = + i0.GeneratedColumn('updated_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _widthMeta = + const i0.VerificationMeta('width'); + @override + late final i0.GeneratedColumn width = i0.GeneratedColumn( + 'width', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _heightMeta = + const i0.VerificationMeta('height'); + @override + late final i0.GeneratedColumn height = i0.GeneratedColumn( + 'height', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _durationInSecondsMeta = + const i0.VerificationMeta('durationInSeconds'); + @override + late final i0.GeneratedColumn durationInSeconds = + i0.GeneratedColumn('duration_in_seconds', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _localIdMeta = + const i0.VerificationMeta('localId'); + @override + late final i0.GeneratedColumn localId = i0.GeneratedColumn( + 'local_id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _checksumMeta = + const i0.VerificationMeta('checksum'); + @override + late final i0.GeneratedColumn checksum = i0.GeneratedColumn( + 'checksum', aliasedName, true, + type: i0.DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + name, + type, + createdAt, + updatedAt, + width, + height, + durationInSeconds, + localId, + checksum + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'local_asset_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + } + if (data.containsKey('width')) { + context.handle( + _widthMeta, width.isAcceptableOrUnknown(data['width']!, _widthMeta)); + } + if (data.containsKey('height')) { + context.handle(_heightMeta, + height.isAcceptableOrUnknown(data['height']!, _heightMeta)); + } + if (data.containsKey('duration_in_seconds')) { + context.handle( + _durationInSecondsMeta, + durationInSeconds.isAcceptableOrUnknown( + data['duration_in_seconds']!, _durationInSecondsMeta)); + } + if (data.containsKey('local_id')) { + context.handle(_localIdMeta, + localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta)); + } else if (isInserting) { + context.missing(_localIdMeta); + } + if (data.containsKey('checksum')) { + context.handle(_checksumMeta, + checksum.isAcceptableOrUnknown(data['checksum']!, _checksumMeta)); + } + return context; + } + + @override + Set get $primaryKey => {localId}; + @override + i1.LocalAssetEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.LocalAssetEntityData( + name: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!, + type: i1.$LocalAssetEntityTable.$convertertype.fromSql(attachedDatabase + .typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}type'])!), + createdAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + width: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}width']), + height: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}height']), + durationInSeconds: attachedDatabase.typeMapping.read( + i0.DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), + localId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}local_id'])!, + checksum: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}checksum']), + ); + } + + @override + $LocalAssetEntityTable createAlias(String alias) { + return $LocalAssetEntityTable(attachedDatabase, alias); + } + + static i0.JsonTypeConverter2 $convertertype = + const i0.EnumIndexConverter(i2.AssetType.values); + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class LocalAssetEntityData extends i0.DataClass + implements i0.Insertable { + final String name; + final i2.AssetType type; + final DateTime createdAt; + final DateTime updatedAt; + final int? width; + final int? height; + final int? durationInSeconds; + final String localId; + final String? checksum; + const LocalAssetEntityData( + {required this.name, + required this.type, + required this.createdAt, + required this.updatedAt, + this.width, + this.height, + this.durationInSeconds, + required this.localId, + this.checksum}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['name'] = i0.Variable(name); + { + map['type'] = i0.Variable( + i1.$LocalAssetEntityTable.$convertertype.toSql(type)); + } + map['created_at'] = i0.Variable(createdAt); + map['updated_at'] = i0.Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = i0.Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = i0.Variable(height); + } + if (!nullToAbsent || durationInSeconds != null) { + map['duration_in_seconds'] = i0.Variable(durationInSeconds); + } + map['local_id'] = i0.Variable(localId); + if (!nullToAbsent || checksum != null) { + map['checksum'] = i0.Variable(checksum); + } + return map; + } + + factory LocalAssetEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return LocalAssetEntityData( + name: serializer.fromJson(json['name']), + type: i1.$LocalAssetEntityTable.$convertertype + .fromJson(serializer.fromJson(json['type'])), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), + durationInSeconds: serializer.fromJson(json['durationInSeconds']), + localId: serializer.fromJson(json['localId']), + checksum: serializer.fromJson(json['checksum']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'name': serializer.toJson(name), + 'type': serializer + .toJson(i1.$LocalAssetEntityTable.$convertertype.toJson(type)), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), + 'durationInSeconds': serializer.toJson(durationInSeconds), + 'localId': serializer.toJson(localId), + 'checksum': serializer.toJson(checksum), + }; + } + + i1.LocalAssetEntityData copyWith( + {String? name, + i2.AssetType? type, + DateTime? createdAt, + DateTime? updatedAt, + i0.Value width = const i0.Value.absent(), + i0.Value height = const i0.Value.absent(), + i0.Value durationInSeconds = const i0.Value.absent(), + String? localId, + i0.Value checksum = const i0.Value.absent()}) => + i1.LocalAssetEntityData( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, + durationInSeconds: durationInSeconds.present + ? durationInSeconds.value + : this.durationInSeconds, + localId: localId ?? this.localId, + checksum: checksum.present ? checksum.value : this.checksum, + ); + LocalAssetEntityData copyWithCompanion(i1.LocalAssetEntityCompanion data) { + return LocalAssetEntityData( + name: data.name.present ? data.name.value : this.name, + type: data.type.present ? data.type.value : this.type, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, + durationInSeconds: data.durationInSeconds.present + ? data.durationInSeconds.value + : this.durationInSeconds, + localId: data.localId.present ? data.localId.value : this.localId, + checksum: data.checksum.present ? data.checksum.value : this.checksum, + ); + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityData(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('localId: $localId, ') + ..write('checksum: $checksum') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(name, type, createdAt, updatedAt, width, + height, durationInSeconds, localId, checksum); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.LocalAssetEntityData && + other.name == this.name && + other.type == this.type && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && + other.durationInSeconds == this.durationInSeconds && + other.localId == this.localId && + other.checksum == this.checksum); +} + +class LocalAssetEntityCompanion + extends i0.UpdateCompanion { + final i0.Value name; + final i0.Value type; + final i0.Value createdAt; + final i0.Value updatedAt; + final i0.Value width; + final i0.Value height; + final i0.Value durationInSeconds; + final i0.Value localId; + final i0.Value checksum; + const LocalAssetEntityCompanion({ + this.name = const i0.Value.absent(), + this.type = const i0.Value.absent(), + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.width = const i0.Value.absent(), + this.height = const i0.Value.absent(), + this.durationInSeconds = const i0.Value.absent(), + this.localId = const i0.Value.absent(), + this.checksum = const i0.Value.absent(), + }); + LocalAssetEntityCompanion.insert({ + required String name, + required i2.AssetType type, + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.width = const i0.Value.absent(), + this.height = const i0.Value.absent(), + this.durationInSeconds = const i0.Value.absent(), + required String localId, + this.checksum = const i0.Value.absent(), + }) : name = i0.Value(name), + type = i0.Value(type), + localId = i0.Value(localId); + static i0.Insertable custom({ + i0.Expression? name, + i0.Expression? type, + i0.Expression? createdAt, + i0.Expression? updatedAt, + i0.Expression? width, + i0.Expression? height, + i0.Expression? durationInSeconds, + i0.Expression? localId, + i0.Expression? checksum, + }) { + return i0.RawValuesInsertable({ + if (name != null) 'name': name, + if (type != null) 'type': type, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, + if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, + if (localId != null) 'local_id': localId, + if (checksum != null) 'checksum': checksum, + }); + } + + i1.LocalAssetEntityCompanion copyWith( + {i0.Value? name, + i0.Value? type, + i0.Value? createdAt, + i0.Value? updatedAt, + i0.Value? width, + i0.Value? height, + i0.Value? durationInSeconds, + i0.Value? localId, + i0.Value? checksum}) { + return i1.LocalAssetEntityCompanion( + name: name ?? this.name, + type: type ?? this.type, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, + durationInSeconds: durationInSeconds ?? this.durationInSeconds, + localId: localId ?? this.localId, + checksum: checksum ?? this.checksum, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (name.present) { + map['name'] = i0.Variable(name.value); + } + if (type.present) { + map['type'] = i0.Variable( + i1.$LocalAssetEntityTable.$convertertype.toSql(type.value)); + } + if (createdAt.present) { + map['created_at'] = i0.Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = i0.Variable(updatedAt.value); + } + if (width.present) { + map['width'] = i0.Variable(width.value); + } + if (height.present) { + map['height'] = i0.Variable(height.value); + } + if (durationInSeconds.present) { + map['duration_in_seconds'] = i0.Variable(durationInSeconds.value); + } + if (localId.present) { + map['local_id'] = i0.Variable(localId.value); + } + if (checksum.present) { + map['checksum'] = i0.Variable(checksum.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('LocalAssetEntityCompanion(') + ..write('name: $name, ') + ..write('type: $type, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') + ..write('durationInSeconds: $durationInSeconds, ') + ..write('localId: $localId, ') + ..write('checksum: $checksum') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/repositories/album_media.repository.dart b/mobile/lib/infrastructure/repositories/album_media.repository.dart new file mode 100644 index 0000000000..e6609cdd5f --- /dev/null +++ b/mobile/lib/infrastructure/repositories/album_media.repository.dart @@ -0,0 +1,66 @@ +import 'package:immich_mobile/constants/constants.dart'; +import 'package:immich_mobile/domain/interfaces/album_media.interface.dart'; +import 'package:immich_mobile/domain/models/asset/asset.model.dart' as asset; +import 'package:photo_manager/photo_manager.dart'; + +class AlbumMediaRepository implements IAlbumMediaRepository { + const AlbumMediaRepository(); + + @override + Future> getAll({PMFilter? filter}) async { + return await PhotoManager.getAssetPathList( + hasAll: true, + filterOption: filter, + ); + } + + @override + Future> getAssetsForAlbum( + AssetPathEntity assetPathEntity, + ) async { + final assets = []; + int pageNumber = 0, lastPageCount = 0; + do { + final page = await assetPathEntity.getAssetListPaged( + page: pageNumber, + size: kFetchLocalAssetsBatchSize, + ); + assets.addAll(page); + lastPageCount = page.length; + pageNumber++; + } while (lastPageCount == kFetchLocalAssetsBatchSize); + return assets.toDtoList(); + } + + @override + Future refresh(String albumId, {PMFilter? filter}) => + AssetPathEntity.obtainPathFromProperties( + id: albumId, + optionGroup: filter, + ); +} + +extension AssetEntityMediaRepoX on AssetEntity { + Future toDto() async { + return asset.LocalAsset( + localId: id, + name: title ?? await titleAsync, + type: switch (type) { + AssetType.other => asset.AssetType.other, + AssetType.image => asset.AssetType.image, + AssetType.video => asset.AssetType.video, + AssetType.audio => asset.AssetType.audio, + }, + createdAt: createDateTime, + updatedAt: modifiedDateTime, + width: width, + height: height, + durationInSeconds: duration, + ); + } +} + +extension AssetEntityListMediaRepoX on List { + Future> toDtoList() => + Future.wait(map((a) => a.toDto())); +} diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index 997714e1b6..7424de5ce7 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -3,6 +3,9 @@ import 'dart:async'; import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; import 'package:immich_mobile/domain/interfaces/db.interface.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'; @@ -25,7 +28,16 @@ class IsarDatabaseRepository implements IDatabaseRepository { Zone.current[_kzoneTxn] == null ? _db.writeTxn(callback) : callback(); } -@DriftDatabase(tables: [UserEntity, UserMetadataEntity, PartnerEntity]) +@DriftDatabase( + tables: [ + UserEntity, + UserMetadataEntity, + PartnerEntity, + LocalAlbumEntity, + LocalAssetEntity, + LocalAlbumAssetEntity, + ], +) class Drift extends $Drift implements IDatabaseRepository { Drift([QueryExecutor? executor]) : super( diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart index a4c2b31dcd..c067b15253 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.drift.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.drift.dart @@ -7,6 +7,12 @@ import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift as i2; import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart' as i3; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' + as i4; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' + as i5; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart' + as i6; abstract class $Drift extends i0.GeneratedDatabase { $Drift(i0.QueryExecutor e) : super(e); @@ -16,12 +22,25 @@ abstract class $Drift extends i0.GeneratedDatabase { i2.$UserMetadataEntityTable(this); late final i3.$PartnerEntityTable partnerEntity = i3.$PartnerEntityTable(this); + late final i4.$LocalAssetEntityTable localAssetEntity = + i4.$LocalAssetEntityTable(this); + late final i5.$LocalAlbumEntityTable localAlbumEntity = + i5.$LocalAlbumEntityTable(this); + late final i6.$LocalAlbumAssetEntityTable localAlbumAssetEntity = + i6.$LocalAlbumAssetEntityTable(this); @override Iterable> get allTables => allSchemaEntities.whereType>(); @override - List get allSchemaEntities => - [userEntity, userMetadataEntity, partnerEntity]; + List get allSchemaEntities => [ + userEntity, + userMetadataEntity, + partnerEntity, + localAssetEntity, + localAlbumEntity, + localAlbumAssetEntity, + i4.localAssetChecksum + ]; @override i0.StreamQueryUpdateRules get streamUpdateRules => const i0.StreamQueryUpdateRules( @@ -48,6 +67,29 @@ abstract class $Drift extends i0.GeneratedDatabase { i0.TableUpdate('partner_entity', kind: i0.UpdateKind.delete), ], ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('local_asset_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('local_album_entity', kind: i0.UpdateKind.update), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('local_asset_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('local_album_asset_entity', + kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('local_album_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('local_album_asset_entity', + kind: i0.UpdateKind.delete), + ], + ), ], ); @override @@ -64,4 +106,10 @@ class $DriftManager { i2.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity); i3.$$PartnerEntityTableTableManager get partnerEntity => i3.$$PartnerEntityTableTableManager(_db, _db.partnerEntity); + i4.$$LocalAssetEntityTableTableManager get localAssetEntity => + i4.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity); + i5.$$LocalAlbumEntityTableTableManager get localAlbumEntity => + i5.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity); + i6.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i6 + .$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity); } diff --git a/mobile/lib/infrastructure/repositories/local_album.repository.dart b/mobile/lib/infrastructure/repositories/local_album.repository.dart new file mode 100644 index 0000000000..b09f48d3c4 --- /dev/null +++ b/mobile/lib/infrastructure/repositories/local_album.repository.dart @@ -0,0 +1,56 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; +import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +class DriftLocalAlbumRepository extends DriftDatabaseRepository + implements ILocalAlbumRepository { + final Drift _db; + const DriftLocalAlbumRepository(this._db) : super(_db); + + @override + Future upsert(LocalAlbum localAlbum) { + final companion = LocalAlbumEntityCompanion.insert( + id: localAlbum.id, + name: localAlbum.name, + updatedAt: Value(localAlbum.updatedAt), + assetCount: Value(localAlbum.assetCount), + thumbnailId: Value.absentIfNull(localAlbum.thumbnailId), + backupSelection: localAlbum.backupSelection, + isAll: Value(localAlbum.isAll), + ); + + return _db.localAlbumEntity + .insertOne(companion, onConflict: DoUpdate((_) => companion)); + } + + @override + Future> getAll({SortLocalAlbumsBy? sortBy}) { + final query = _db.localAlbumEntity.select(); + if (sortBy == SortLocalAlbumsBy.id) { + query.orderBy([(a) => OrderingTerm.asc(a.id)]); + } + return query.map((a) => a.toDto()).get(); + } + + @override + Future delete(String albumId) => _db.managers.localAlbumEntity + .filter((a) => a.id.equals(albumId)) + .delete(); + + @override + Future> getAssetIdsOnlyInAlbum(String albumId) { + final assetId = _db.localAlbumAssetEntity.assetId; + final query = _db.localAlbumAssetEntity.selectOnly() + ..addColumns([assetId]) + ..groupBy( + [assetId], + having: _db.localAlbumAssetEntity.albumId.count().equals(1) & + _db.localAlbumAssetEntity.albumId.equals(albumId), + ); + + return query.map((row) => row.read(assetId)!).get(); + } +} diff --git a/mobile/lib/infrastructure/repositories/local_album_asset.repository.dart b/mobile/lib/infrastructure/repositories/local_album_asset.repository.dart new file mode 100644 index 0000000000..be951ec8b1 --- /dev/null +++ b/mobile/lib/infrastructure/repositories/local_album_asset.repository.dart @@ -0,0 +1,55 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/interfaces/local_album_asset.interface.dart'; +import 'package:immich_mobile/domain/models/asset/asset.model.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +class DriftLocalAlbumAssetRepository extends DriftDatabaseRepository + implements ILocalAlbumAssetRepository { + final Drift _db; + const DriftLocalAlbumAssetRepository(super._db) : _db = _db; + + @override + Future linkAssetsToAlbum(String albumId, Iterable assetIds) => + _db.batch( + (batch) => batch.insertAll( + _db.localAlbumAssetEntity, + assetIds.map( + (a) => LocalAlbumAssetEntityCompanion.insert( + assetId: a, + albumId: albumId, + ), + ), + mode: InsertMode.insertOrIgnore, + ), + ); + + @override + Future> getAssetsForAlbum(String albumId) { + final query = _db.localAlbumAssetEntity.select().join( + [ + innerJoin( + _db.localAssetEntity, + _db.localAlbumAssetEntity.assetId + .equalsExp(_db.localAssetEntity.localId), + ), + ], + )..where(_db.localAlbumAssetEntity.albumId.equals(albumId)); + return query + .map((row) => row.readTable(_db.localAssetEntity).toDto()) + .get(); + } + + @override + Future unlinkAssetsFromAlbum( + String albumId, + Iterable assetIds, + ) => + _db.batch( + (batch) => batch.deleteWhere( + _db.localAlbumAssetEntity, + (f) => f.assetId.isIn(assetIds), + ), + ); +} diff --git a/mobile/lib/infrastructure/repositories/local_asset.repository.dart b/mobile/lib/infrastructure/repositories/local_asset.repository.dart new file mode 100644 index 0000000000..61bf93d521 --- /dev/null +++ b/mobile/lib/infrastructure/repositories/local_asset.repository.dart @@ -0,0 +1,47 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/interfaces/local_asset.interface.dart'; +import 'package:immich_mobile/domain/models/asset/asset.model.dart'; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +class DriftLocalAssetRepository extends DriftDatabaseRepository + implements ILocalAssetRepository { + final Drift _db; + const DriftLocalAssetRepository(this._db) : super(_db); + + @override + Future deleteIds(Iterable ids) => _db.batch( + (batch) => batch.deleteWhere( + _db.localAssetEntity, + (f) => f.localId.isIn(ids), + ), + ); + + @override + Future upsertAll(Iterable localAssets) => + _db.batch((batch) async { + batch.insertAllOnConflictUpdate( + _db.localAssetEntity, + localAssets.map( + (a) => LocalAssetEntityCompanion.insert( + name: a.name, + type: a.type, + createdAt: Value(a.createdAt), + updatedAt: Value(a.updatedAt), + width: Value.absentIfNull(a.width), + height: Value.absentIfNull(a.height), + durationInSeconds: Value.absentIfNull(a.durationInSeconds), + localId: a.localId, + checksum: Value.absentIfNull(a.checksum), + ), + ), + ); + }); + + @override + Future get(String assetId) => _db.managers.localAssetEntity + .filter((f) => f.localId(assetId)) + .map((a) => a.toDto()) + .getSingle(); +} diff --git a/mobile/lib/infrastructure/utils/asset.mixin.dart b/mobile/lib/infrastructure/utils/asset.mixin.dart new file mode 100644 index 0000000000..e632b0a49d --- /dev/null +++ b/mobile/lib/infrastructure/utils/asset.mixin.dart @@ -0,0 +1,12 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/asset/asset.model.dart'; + +mixin AssetEntityMixin on Table { + TextColumn get name => text()(); + IntColumn get type => intEnum()(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + IntColumn get width => integer().nullable()(); + IntColumn get height => integer().nullable()(); + IntColumn get durationInSeconds => integer().nullable()(); +} diff --git a/mobile/lib/utils/diff.dart b/mobile/lib/utils/diff.dart index ea20de16cc..67a1f28e53 100644 --- a/mobile/lib/utils/diff.dart +++ b/mobile/lib/utils/diff.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid-unsafe-collection-methods + import 'dart:async'; import 'package:collection/collection.dart'; diff --git a/mobile/test/domain/services/hash_service_test.dart b/mobile/test/domain/services/hash_service_test.dart index 2da41cd704..fdf2d0b9a2 100644 --- a/mobile/test/domain/services/hash_service_test.dart +++ b/mobile/test/domain/services/hash_service_test.dart @@ -12,16 +12,14 @@ import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/hash.service.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:photo_manager/photo_manager.dart'; +import '../../external.mock.dart'; import '../../fixtures/asset.stub.dart'; import '../../infrastructure/repository.mock.dart'; import '../../service.mocks.dart'; class MockAsset extends Mock implements Asset {} -class MockAssetEntity extends Mock implements AssetEntity {} - void main() { late HashService sut; late BackgroundService mockBackgroundService; diff --git a/mobile/test/domain/services/sync_service_test.dart b/mobile/test/domain/services/sync_service_test.dart new file mode 100644 index 0000000000..3c7f253743 --- /dev/null +++ b/mobile/test/domain/services/sync_service_test.dart @@ -0,0 +1,361 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/interfaces/album_media.interface.dart'; +import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; +import 'package:immich_mobile/domain/interfaces/local_album_asset.interface.dart'; +import 'package:immich_mobile/domain/interfaces/local_asset.interface.dart'; +import 'package:immich_mobile/domain/models/asset/asset.model.dart' + hide AssetType; +import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/domain/services/sync.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/album_media.repository.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:photo_manager/photo_manager.dart'; + +import '../../external.mock.dart'; +import '../../fixtures/local_album.stub.dart'; +import '../../fixtures/local_asset.stub.dart'; +import '../../infrastructure/repository.mock.dart'; + +void main() { + group('SyncService', () { + late SyncService sut; + late IAlbumMediaRepository mockAlbumMediaRepo; + late ILocalAlbumRepository mockLocalAlbumRepo; + late ILocalAssetRepository mockLocalAssetRepo; + late ILocalAlbumAssetRepository mockLocalAlbumAssetRepo; + + const albumId = 'test-album-id'; + final now = DateTime.now(); + final earlier = now.subtract(const Duration(days: 1)); + + late LocalAlbum dbAlbum; + late LocalAlbum deviceAlbum; + late AssetPathEntity deviceAlbumEntity; + late List deviceAssets; + late List dbAssets; + + setUp(() async { + mockAlbumMediaRepo = MockAlbumMediaRepository(); + mockLocalAlbumRepo = MockLocalAlbumRepository(); + mockLocalAssetRepo = MockLocalAssetRepository(); + mockLocalAlbumAssetRepo = MockLocalAlbumAssetRepository(); + + sut = SyncService( + albumMediaRepository: mockAlbumMediaRepo, + localAlbumRepository: mockLocalAlbumRepo, + localAssetRepository: mockLocalAssetRepo, + localAlbumAssetRepository: mockLocalAlbumAssetRepo, + ); + + dbAlbum = LocalAlbum( + id: albumId, + name: 'Test Album', + updatedAt: earlier, + assetCount: 5, + backupSelection: BackupSelection.none, + ); + + deviceAlbumEntity = MockAssetPathEntity(); + when(() => deviceAlbumEntity.id).thenReturn(albumId); + when(() => deviceAlbumEntity.name).thenReturn('Test Album'); + when(() => deviceAlbumEntity.lastModified).thenReturn(now); + when(() => deviceAlbumEntity.isAll).thenReturn(false); + when(() => deviceAlbumEntity.assetCountAsync).thenAnswer((_) async => 5); + deviceAlbum = await deviceAlbumEntity.toDto(); + + deviceAssets = await Future.wait( + List.generate(5, (i) { + final asset = MockAssetEntity(); + when(() => asset.id).thenReturn('asset-$i'); + when(() => asset.title).thenReturn('Asset $i'); + when(() => asset.createDateTime).thenReturn(now); + when(() => asset.modifiedDateTime).thenReturn(now); + when(() => asset.width).thenReturn(1920); + when(() => asset.height).thenReturn(1080); + when(() => asset.type).thenReturn(AssetType.image); + when(() => asset.duration).thenReturn(0); + return asset.toDto(); + }), + ); + + dbAssets = await Future.wait( + List.generate(5, (i) { + final asset = MockAssetEntity(); + when(() => asset.id).thenReturn('asset-$i'); + when(() => asset.title).thenReturn('Asset $i'); + when(() => asset.createDateTime).thenReturn(earlier); + when(() => asset.modifiedDateTime).thenReturn(earlier); + when(() => asset.width).thenReturn(1920); + when(() => asset.height).thenReturn(1080); + when(() => asset.type).thenReturn(AssetType.image); + when(() => asset.duration).thenReturn(0); + return asset.toDto(); + }), + ); + + registerFallbackValue(FakeAssetEntity()); + registerFallbackValue(FakeAssetPathEntity()); + registerFallbackValue(LocalAssetStub.image1); + registerFallbackValue(LocalAlbumStub.album1); + + when(() => mockAlbumMediaRepo.refresh(albumId)) + .thenAnswer((_) async => deviceAlbumEntity); + + when( + () => mockAlbumMediaRepo.refresh( + albumId, + filter: any(named: 'filter'), + ), + ).thenAnswer((_) async => deviceAlbumEntity); + + when(() => mockAlbumMediaRepo.getAssetsForAlbum(deviceAlbumEntity)) + .thenAnswer((_) async => deviceAssets); + + when(() => mockLocalAlbumAssetRepo.getAssetsForAlbum(albumId)) + .thenAnswer((_) async => dbAssets); + + when(() => mockLocalAssetRepo.upsertAll(any())) + .thenAnswer((_) async => {}); + + when(() => mockLocalAlbumAssetRepo.linkAssetsToAlbum(any(), any())) + .thenAnswer((_) async => {}); + + when(() => mockLocalAlbumRepo.upsert(any())).thenAnswer((_) async => {}); + + when(() => mockLocalAlbumRepo.transaction(any())) + .thenAnswer((_) async { + final capturedCallback = verify( + () => mockLocalAlbumRepo.transaction(captureAny()), + ).captured; + // Invoke the transaction callback + await (capturedCallback.firstOrNull as Future Function()?) + ?.call(); + }); + }); + + test( + 'album filter should be properly configured with expected settings', + () { + // Access the album filter from the service + final albumFilter = sut.albumFilter; + + // Verify image option settings + final imageOption = albumFilter.getOption(AssetType.image); + expect(imageOption.needTitle, isTrue); + expect(imageOption.sizeConstraint.ignoreSize, isTrue); + + // Verify video option settings + final videoOption = albumFilter.getOption(AssetType.video); + expect(videoOption.needTitle, isTrue); + expect(videoOption.sizeConstraint.ignoreSize, isTrue); + expect(videoOption.durationConstraint.allowNullable, isTrue); + + // Verify containsPathModified flag + expect(albumFilter.containsPathModified, isTrue); + + // Verify time conditions are ignored + expect(albumFilter.createTimeCond.ignore, isTrue); + expect(albumFilter.updateTimeCond.ignore, isTrue); + + // Verify ordering + expect(albumFilter.orders.length, 1); + expect( + albumFilter.orders.firstOrNull?.type, + OrderOptionType.createDate, + ); + expect(albumFilter.orders.firstOrNull?.asc, isFalse); + }, + ); + + group('handleOnlyAssetsAdded: ', () { + // All the below tests expects the device album to have more assets + // than the DB album. This is to simulate the scenario where + // new assets are added to the device album. + setUp(() { + deviceAlbum = deviceAlbum.copyWith(assetCount: 10); + }); + + test( + 'early return when device album timestamp is not after DB album', + () async { + final result = await sut.handleOnlyAssetsAdded( + dbAlbum, + deviceAlbum.copyWith(updatedAt: earlier), + ); + + // Verify: method returns without making any changes + expect(result, isFalse); + verifyNever(() => mockAlbumMediaRepo.getAssetsForAlbum(any())); + verifyNever(() => mockLocalAssetRepo.upsertAll(any())); + verifyNever( + () => mockLocalAlbumAssetRepo.linkAssetsToAlbum(any(), any()), + ); + verifyNever(() => mockLocalAlbumRepo.upsert(any())); + }, + ); + + test( + 'early return when device album has fewer assets than DB album', + () async { + // Execute + final result = await sut.handleOnlyAssetsAdded( + dbAlbum, + deviceAlbum.copyWith(assetCount: dbAlbum.assetCount - 1), + ); + + expect(result, isFalse); + verifyNever(() => mockAlbumMediaRepo.getAssetsForAlbum(any())); + verifyNever(() => mockLocalAssetRepo.upsertAll(any())); + verifyNever( + () => mockLocalAlbumAssetRepo.linkAssetsToAlbum(any(), any()), + ); + verifyNever(() => mockLocalAlbumRepo.upsert(any())); + }, + ); + + test( + 'correctly processes assets when new assets are added', + () async { + final result = await sut.handleOnlyAssetsAdded(dbAlbum, deviceAlbum); + + verify( + () => mockAlbumMediaRepo.getAssetsForAlbum(deviceAlbumEntity), + ).called(1); + + verify(() => mockLocalAssetRepo.upsertAll(any())).called(1); + verify( + () => mockLocalAlbumAssetRepo.linkAssetsToAlbum( + albumId, + deviceAssets.map((a) => a.localId), + ), + ).called(1); + + verify(() => mockLocalAlbumRepo.upsert(any())).called(1); + + expect(result, isTrue); + }, + ); + + test( + 'correct handling when filtering yields no new assets', + () async { + when(() => mockAlbumMediaRepo.getAssetsForAlbum(deviceAlbumEntity)) + .thenAnswer((_) async => deviceAssets.sublist(0, 2)); + + final result = await sut.handleOnlyAssetsAdded(dbAlbum, deviceAlbum); + + verify(() => mockAlbumMediaRepo.getAssetsForAlbum(deviceAlbumEntity)) + .called(1); + + verifyNever(() => mockLocalAssetRepo.upsertAll(any())); + verifyNever( + () => mockLocalAlbumAssetRepo.linkAssetsToAlbum(any(), any()), + ); + verifyNever(() => mockLocalAlbumRepo.upsert(any())); + + expect(result, isFalse); + }, + ); + + test( + 'thumbnail is updated when new asset is newer than existing thumbnail', + () async { + final oldThumbnailId = 'asset-100'; + when(() => mockLocalAssetRepo.get(oldThumbnailId)).thenAnswer( + (_) async => + LocalAssetStub.image1.copyWith(createdAt: DateTime(100)), + ); + + final result = await sut.handleOnlyAssetsAdded( + dbAlbum.copyWith(thumbnailId: oldThumbnailId), + deviceAlbum, + ); + + final capturedAlbum = + verify(() => mockLocalAlbumRepo.upsert(captureAny())) + .captured + .singleOrNull as LocalAlbum?; + expect(capturedAlbum?.thumbnailId, isNot(equals(oldThumbnailId))); + expect( + capturedAlbum?.thumbnailId, + equals(deviceAssets.firstOrNull?.localId), + ); + expect(result, isTrue); + }, + ); + + test( + 'thumbnail preservation when new asset is older than existing thumbnail', + () async { + final oldThumbnailId = 'asset-100'; + when(() => mockLocalAssetRepo.get(oldThumbnailId)).thenAnswer( + (_) async => LocalAssetStub.image1 + .copyWith(createdAt: now.add(const Duration(days: 1))), + ); + + final result = await sut.handleOnlyAssetsAdded( + dbAlbum.copyWith(thumbnailId: oldThumbnailId), + deviceAlbum, + ); + + final capturedAlbum = + verify(() => mockLocalAlbumRepo.upsert(captureAny())) + .captured + .singleOrNull as LocalAlbum?; + expect(capturedAlbum?.thumbnailId, equals(oldThumbnailId)); + expect(result, isTrue); + }, + ); + }); + + group('addLocalAlbum: ', () { + test('adding an album with no assets works correctly', () async { + when(() => deviceAlbumEntity.assetCountAsync) + .thenAnswer((_) async => 0); + + await sut.addLocalAlbum(deviceAlbum.copyWith(assetCount: 0)); + + final albumUpsertCall = + verify(() => mockLocalAlbumRepo.upsert(captureAny())); + albumUpsertCall.called(1); + + // Always refreshed + verify( + () => mockAlbumMediaRepo.refresh(albumId, filter: sut.albumFilter), + ).called(1); + verifyNever(() => mockLocalAssetRepo.upsertAll(any())); + verifyNever( + () => mockLocalAlbumAssetRepo.linkAssetsToAlbum(any(), any()), + ); + + final capturedAlbum = + albumUpsertCall.captured.singleOrNull as LocalAlbum?; + expect(capturedAlbum?.id, equals(albumId)); + expect(capturedAlbum?.name, equals('Test Album')); + expect(capturedAlbum?.assetCount, equals(0)); + expect(capturedAlbum?.thumbnailId, isNull); + }); + + test( + 'adding an album with multiple assets works correctly', + () async { + await sut.addLocalAlbum(deviceAlbum); + + final albumUpsertCall = + verify(() => mockLocalAlbumRepo.upsert(captureAny())); + albumUpsertCall.called(1); + verify(() => mockLocalAssetRepo.upsertAll(any())).called(1); + verify( + () => mockLocalAlbumAssetRepo.linkAssetsToAlbum(albumId, any()), + ).called(1); + + final capturedAlbum = + albumUpsertCall.captured.singleOrNull as LocalAlbum?; + expect(capturedAlbum?.assetCount, deviceAssets.length); + + expect(capturedAlbum?.thumbnailId, deviceAssets.firstOrNull?.localId); + }, + ); + }); + }); +} diff --git a/mobile/test/external.mock.dart b/mobile/test/external.mock.dart new file mode 100644 index 0000000000..3c49386163 --- /dev/null +++ b/mobile/test/external.mock.dart @@ -0,0 +1,10 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:photo_manager/photo_manager.dart'; + +class MockAssetEntity extends Mock implements AssetEntity {} + +class FakeAssetEntity extends Fake implements AssetEntity {} + +class MockAssetPathEntity extends Mock implements AssetPathEntity {} + +class FakeAssetPathEntity extends Fake implements AssetPathEntity {} diff --git a/mobile/test/fixtures/local_album.stub.dart b/mobile/test/fixtures/local_album.stub.dart new file mode 100644 index 0000000000..c58e43417c --- /dev/null +++ b/mobile/test/fixtures/local_album.stub.dart @@ -0,0 +1,15 @@ +import 'package:immich_mobile/domain/models/local_album.model.dart'; + +abstract final class LocalAlbumStub { + const LocalAlbumStub(); + + static LocalAlbum get album1 => LocalAlbum( + id: "album1", + name: "Album 1", + updatedAt: DateTime(2023), + assetCount: 1, + thumbnailId: null, + backupSelection: BackupSelection.none, + isAll: false, + ); +} diff --git a/mobile/test/fixtures/local_asset.stub.dart b/mobile/test/fixtures/local_asset.stub.dart new file mode 100644 index 0000000000..f1a0e17118 --- /dev/null +++ b/mobile/test/fixtures/local_asset.stub.dart @@ -0,0 +1,17 @@ +import 'package:immich_mobile/domain/models/asset/asset.model.dart'; + +abstract final class LocalAssetStub { + const LocalAssetStub(); + + static LocalAsset get image1 => LocalAsset( + localId: "image1", + name: "image1.jpg", + checksum: "image1-checksum", + type: AssetType.image, + createdAt: DateTime(2019), + updatedAt: DateTime.now(), + width: 1920, + height: 1080, + durationInSeconds: 0, + ); +} diff --git a/mobile/test/infrastructure/repository.mock.dart b/mobile/test/infrastructure/repository.mock.dart index c4a5680f71..6fe68d432a 100644 --- a/mobile/test/infrastructure/repository.mock.dart +++ b/mobile/test/infrastructure/repository.mock.dart @@ -1,4 +1,8 @@ +import 'package:immich_mobile/domain/interfaces/album_media.interface.dart'; import 'package:immich_mobile/domain/interfaces/device_asset.interface.dart'; +import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; +import 'package:immich_mobile/domain/interfaces/local_album_asset.interface.dart'; +import 'package:immich_mobile/domain/interfaces/local_asset.interface.dart'; import 'package:immich_mobile/domain/interfaces/log.interface.dart'; import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart'; @@ -18,6 +22,15 @@ class MockDeviceAssetRepository extends Mock class MockSyncStreamRepository extends Mock implements ISyncStreamRepository {} +class MockLocalAssetRepository extends Mock implements ILocalAssetRepository {} + +class MockLocalAlbumRepository extends Mock implements ILocalAlbumRepository {} + +class MockAlbumMediaRepository extends Mock implements IAlbumMediaRepository {} + +class MockLocalAlbumAssetRepository extends Mock + implements ILocalAlbumAssetRepository {} + // API Repos class MockUserApiRepository extends Mock implements IUserApiRepository {} diff --git a/mobile/test/modules/shared/sync_service_test.dart b/mobile/test/modules/shared/sync_service_test.dart index 2029ade018..027107d731 100644 --- a/mobile/test/modules/shared/sync_service_test.dart +++ b/mobile/test/modules/shared/sync_service_test.dart @@ -17,7 +17,8 @@ import 'package:mocktail/mocktail.dart'; import '../../domain/service.mock.dart'; import '../../fixtures/asset.stub.dart'; -import '../../infrastructure/repository.mock.dart'; +import '../../infrastructure/repository.mock.dart' + hide MockAlbumMediaRepository; import '../../repository.mocks.dart'; import '../../service.mocks.dart'; import '../../test_utils.dart';