diff --git a/mobile/lib/domain/interfaces/sync_stream.interface.dart b/mobile/lib/domain/interfaces/sync_stream.interface.dart index 5f61d6b52..502cf0ad4 100644 --- a/mobile/lib/domain/interfaces/sync_stream.interface.dart +++ b/mobile/lib/domain/interfaces/sync_stream.interface.dart @@ -15,4 +15,13 @@ abstract interface class ISyncStreamRepository implements IDatabaseRepository { Future updatePartnerAssetsV1(Iterable data); Future deletePartnerAssetsV1(Iterable data); Future updatePartnerAssetsExifV1(Iterable data); + + Future updateAlbumsV1(Iterable data); + Future deleteAlbumsV1(Iterable data); + + // Future updateAlbumAssetsV1(Iterable data); + // Future deleteAlbumAssetsV1(Iterable data); + + Future updateAlbumUsersV1(Iterable data); + Future deleteAlbumUsersV1(Iterable data); } diff --git a/mobile/lib/domain/models/album/album.model.dart b/mobile/lib/domain/models/album/album.model.dart new file mode 100644 index 000000000..47683121b --- /dev/null +++ b/mobile/lib/domain/models/album/album.model.dart @@ -0,0 +1,68 @@ +enum AssetOrder { + // do not change this order! + asc, + desc, +} + +// Model for an album stored in the server +class Album { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final AssetOrder order; + + const Album({ + required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order, + }); + + @override + String toString() { + return '''Album { + id: $id, + name: $name, + description: $description, + createdAt: $createdAt, + updatedAt: $updatedAt, + isActivityEnabled: $isActivityEnabled, + order: $order, + thumbnailAssetId: ${thumbnailAssetId ?? ""} + }'''; + } + + @override + bool operator ==(Object other) { + if (other is! Album) return false; + if (identical(this, other)) return true; + return id == other.id && + name == other.name && + description == other.description && + createdAt == other.createdAt && + updatedAt == other.updatedAt && + thumbnailAssetId == other.thumbnailAssetId && + isActivityEnabled == other.isActivityEnabled && + order == other.order; + } + + @override + int get hashCode { + return id.hashCode ^ + name.hashCode ^ + description.hashCode ^ + createdAt.hashCode ^ + updatedAt.hashCode ^ + thumbnailAssetId.hashCode ^ + isActivityEnabled.hashCode ^ + order.hashCode; + } +} diff --git a/mobile/lib/domain/models/album_user.model.dart b/mobile/lib/domain/models/album_user.model.dart new file mode 100644 index 000000000..10c03a7c4 --- /dev/null +++ b/mobile/lib/domain/models/album_user.model.dart @@ -0,0 +1,5 @@ +enum AlbumUserRole { + // do not change this order! + editor, + viewer, +} diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index 99d7de6cc..f997bef13 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -81,6 +81,18 @@ class SyncStreamService { return _syncStreamRepository.deletePartnerAssetsV1(data.cast()); case SyncEntityType.partnerAssetExifV1: return _syncStreamRepository.updatePartnerAssetsExifV1(data.cast()); + case SyncEntityType.albumV1: + return _syncStreamRepository.updateAlbumsV1(data.cast()); + case SyncEntityType.albumDeleteV1: + return _syncStreamRepository.deleteAlbumsV1(data.cast()); + // case SyncEntityType.albumAssetV1: + // return _syncStreamRepository.updateAlbumAssetsV1(data.cast()); + // case SyncEntityType.albumAssetDeleteV1: + // return _syncStreamRepository.deleteAlbumAssetsV1(data.cast()); + case SyncEntityType.albumUserV1: + return _syncStreamRepository.updateAlbumUsersV1(data.cast()); + case SyncEntityType.albumUserDeleteV1: + return _syncStreamRepository.deleteAlbumUsersV1(data.cast()); default: _logger.warning("Unknown sync data type: $type"); } diff --git a/mobile/lib/infrastructure/entities/album_user.entity.dart b/mobile/lib/infrastructure/entities/album_user.entity.dart new file mode 100644 index 000000000..c9bbfbd22 --- /dev/null +++ b/mobile/lib/infrastructure/entities/album_user.entity.dart @@ -0,0 +1,20 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/album_user.model.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class AlbumUserEntity extends Table with DriftDefaultsMixin { + const AlbumUserEntity(); + + TextColumn get albumId => + text().references(RemoteAlbumEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get userId => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + + IntColumn get role => intEnum()(); + + @override + Set get primaryKey => {albumId, userId}; +} diff --git a/mobile/lib/infrastructure/entities/album_user.entity.drift.dart b/mobile/lib/infrastructure/entities/album_user.entity.drift.dart new file mode 100644 index 000000000..008e0bb52 Binary files /dev/null and b/mobile/lib/infrastructure/entities/album_user.entity.drift.dart differ diff --git a/mobile/lib/infrastructure/entities/remote_album.entity.dart b/mobile/lib/infrastructure/entities/remote_album.entity.dart new file mode 100644 index 000000000..7f9f1efa6 --- /dev/null +++ b/mobile/lib/infrastructure/entities/remote_album.entity.dart @@ -0,0 +1,34 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class RemoteAlbumEntity extends Table with DriftDefaultsMixin { + const RemoteAlbumEntity(); + + TextColumn get id => text()(); + + TextColumn get name => text()(); + + TextColumn get description => text()(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + TextColumn get ownerId => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get thumbnailAssetId => text() + .references(RemoteAssetEntity, #id, onDelete: KeyAction.setNull) + .nullable()(); + + BoolColumn get isActivityEnabled => + boolean().withDefault(const Constant(true))(); + + IntColumn get order => intEnum()(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/remote_album.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_album.entity.drift.dart new file mode 100644 index 000000000..cc7b642c9 Binary files /dev/null and b/mobile/lib/infrastructure/entities/remote_album.entity.drift.dart differ diff --git a/mobile/lib/infrastructure/entities/remote_album_asset.entity.dart b/mobile/lib/infrastructure/entities/remote_album_asset.entity.dart new file mode 100644 index 000000000..1dcc336ed --- /dev/null +++ b/mobile/lib/infrastructure/entities/remote_album_asset.entity.dart @@ -0,0 +1,17 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class RemoteAlbumAssetEntity extends Table with DriftDefaultsMixin { + const RemoteAlbumAssetEntity(); + + TextColumn get assetId => + text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get albumId => + text().references(RemoteAlbumEntity, #id, onDelete: KeyAction.cascade)(); + + @override + Set get primaryKey => {assetId, albumId}; +} diff --git a/mobile/lib/infrastructure/entities/remote_album_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_album_asset.entity.drift.dart new file mode 100644 index 000000000..ab50607c9 Binary files /dev/null and b/mobile/lib/infrastructure/entities/remote_album_asset.entity.drift.dart differ diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index 4ad60276a..f414cd9f6 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -3,12 +3,15 @@ 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/album_user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.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/remote_album.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_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'; import 'package:isar/isar.dart'; @@ -38,8 +41,11 @@ class IsarDatabaseRepository implements IDatabaseRepository { LocalAlbumEntity, LocalAssetEntity, LocalAlbumAssetEntity, - RemoteAssetEntity, RemoteExifEntity, + RemoteAssetEntity, + RemoteAlbumEntity, + RemoteAlbumAssetEntity, + AlbumUserEntity, ], ) class Drift extends $Drift implements IDatabaseRepository { diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart index d1bda9365..54c12ecde 100644 Binary files a/mobile/lib/infrastructure/repositories/db.repository.drift.dart and b/mobile/lib/infrastructure/repositories/db.repository.drift.dart differ diff --git a/mobile/lib/infrastructure/repositories/sync_api.repository.dart b/mobile/lib/infrastructure/repositories/sync_api.repository.dart index ca24eef60..70f940c10 100644 --- a/mobile/lib/infrastructure/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -50,6 +50,9 @@ class SyncApiRepository implements ISyncApiRepository { SyncRequestType.partnerAssetsV1, SyncRequestType.assetExifsV1, SyncRequestType.partnerAssetExifsV1, + SyncRequestType.albumsV1, + // SyncRequestType.albumAssetsV1, + SyncRequestType.albumUsersV1, ], ).toJson(), ); @@ -140,4 +143,10 @@ const _kResponseMap = { SyncEntityType.partnerAssetV1: SyncAssetV1.fromJson, SyncEntityType.partnerAssetDeleteV1: SyncAssetDeleteV1.fromJson, SyncEntityType.partnerAssetExifV1: SyncAssetExifV1.fromJson, + SyncEntityType.albumV1: SyncAlbumV1.fromJson, + SyncEntityType.albumDeleteV1: SyncAlbumDeleteV1.fromJson, + // SyncEntityType.albumAssetV1: SyncAlbumAssetV1.fromJson, + // SyncEntityType.albumAssetDeleteV1: SyncAlbumAssetDeleteV1.fromJson, + SyncEntityType.albumUserV1: SyncAlbumUserV1.fromJson, + SyncEntityType.albumUserDeleteV1: SyncAlbumUserDeleteV1.fromJson, }; diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index 7aa8fc6ef..472bc9a6b 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -3,12 +3,19 @@ import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/domain/models/album_user.model.dart'; +import 'package:immich_mobile/infrastructure/entities/album_user.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'; +// import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart' as api show AssetVisibility; -import 'package:openapi/api.dart' hide AssetVisibility; +import 'package:openapi/api.dart' as api + show AssetVisibility, AssetOrder, AlbumUserRole; +import 'package:openapi/api.dart' + hide AssetVisibility, AssetOrder, AlbumUserRole; class DriftSyncStreamRepository extends DriftDatabaseRepository implements ISyncStreamRepository { @@ -161,6 +168,135 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository } } + @override + Future updateAlbumsV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final album in data) { + final companion = RemoteAlbumEntityCompanion( + name: Value(album.name), + description: Value(album.description), + ownerId: Value(album.ownerId), + thumbnailAssetId: Value(album.thumbnailAssetId), + createdAt: Value(album.createdAt), + updatedAt: Value(album.updatedAt), + isActivityEnabled: Value(album.isActivityEnabled), + order: Value(album.order.toAssetOrder()), + ); + + batch.insert( + _db.remoteAlbumEntity, + companion.copyWith(id: Value(album.id)), + onConflict: DoUpdate((_) => companion), + ); + } + }); + } catch (e, s) { + _logger.severe('Error while processing updateAlbumsV1', e, s); + rethrow; + } + } + + @override + Future deleteAlbumsV1(Iterable data) async { + try { + _db.batch((batch) { + for (final album in data) { + batch.delete( + _db.remoteAlbumEntity, + RemoteAlbumEntityCompanion(id: Value(album.albumId)), + ); + } + }); + } catch (e, s) { + _logger.severe('Error while processing deleteAlbumsV1', e, s); + rethrow; + } + } + + // @override + // Future updateAlbumAssetsV1(Iterable data) async { + // try { + // await _db.remoteAlbumAssetEntity.insertAll( + // data.map( + // (albumAsset) => RemoteAlbumAssetEntityCompanion.insert( + // albumId: albumAsset.albumId, + // assetId: albumAsset.assetId, + // ), + // ), + // mode: InsertMode.insertOrIgnore, + // ); + // } catch (e, s) { + // _logger.severe('Error while processing updateAlbumAssetsV1', e, s); + // rethrow; + // } + // } + + // @override + // Future deleteAlbumAssetsV1(Iterable data) async { + // try { + // await _db.batch((batch) { + // for (final albumAsset in data) { + // batch.delete( + // _db.remoteAlbumAssetEntity, + // RemoteAlbumAssetEntityCompanion( + // albumId: Value(albumAsset.albumId), + // assetId: Value(albumAsset.assetId), + // ), + // ); + // } + // }); + // } catch (e, s) { + // _logger.severe('Error while processing deleteAlbumAssetsV1', e, s); + // rethrow; + // } + // } + + @override + Future updateAlbumUsersV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final albumUser in data) { + final companion = AlbumUserEntityCompanion( + role: Value(albumUser.role.toAlbumUserRole()), + ); + + batch.insert( + _db.albumUserEntity, + companion.copyWith( + albumId: Value(albumUser.albumId), + userId: Value(albumUser.userId), + ), + onConflict: DoUpdate((_) => companion), + ); + } + }); + } catch (e, s) { + _logger.severe('Error while processing updateAlbumUsersV1', e, s); + rethrow; + } + } + + @override + Future deleteAlbumUsersV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final albumUser in data) { + batch.delete( + _db.albumUserEntity, + AlbumUserEntityCompanion( + albumId: Value(albumUser.albumId), + userId: Value(albumUser.userId), + ), + ); + } + }); + } catch (e, s) { + _logger.severe('Error while processing deleteAlbumUsersV1', e, s); + rethrow; + } + } + Future _updateAssetsV1(Iterable data) => _db.batch((batch) { for (final asset in data) { @@ -251,3 +387,19 @@ extension on api.AssetVisibility { _ => throw Exception('Unknown AssetVisibility value: $this'), }; } + +extension on api.AssetOrder { + AssetOrder toAssetOrder() => switch (this) { + api.AssetOrder.asc => AssetOrder.asc, + api.AssetOrder.desc => AssetOrder.desc, + _ => throw Exception('Unknown AssetOrder value: $this'), + }; +} + +extension on api.AlbumUserRole { + AlbumUserRole toAlbumUserRole() => switch (this) { + api.AlbumUserRole.editor => AlbumUserRole.editor, + api.AlbumUserRole.viewer => AlbumUserRole.viewer, + _ => throw Exception('Unknown AlbumUserRole value: $this'), + }; +} diff --git a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart index edbbd2379..0e532375d 100644 --- a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart +++ b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart @@ -63,8 +63,10 @@ final _features = [ icon: Icons.delete_sweep_rounded, onTap: (_, ref) async { final db = ref.read(driftProvider); - await db.remoteAssetEntity.deleteAll(); await db.remoteExifEntity.deleteAll(); + await db.remoteAssetEntity.deleteAll(); + await db.remoteAlbumEntity.deleteAll(); + await db.remoteAlbumAssetEntity.deleteAll(); }, ), _Feature( diff --git a/mobile/lib/presentation/pages/dev/media_stat.page.dart b/mobile/lib/presentation/pages/dev/media_stat.page.dart index c074e524b..4a1562900 100644 --- a/mobile/lib/presentation/pages/dev/media_stat.page.dart +++ b/mobile/lib/presentation/pages/dev/media_stat.page.dart @@ -132,6 +132,10 @@ final _remoteStats = [ name: 'Exif Entities', load: (db) => db.managers.remoteExifEntity.count(), ), + _Stat( + name: 'Remote Albums', + load: (db) => db.managers.remoteAlbumEntity.count(), + ), ]; @RoutePage() diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart index 01d2684fa..69a2fa024 100644 --- a/mobile/lib/repositories/auth.repository.dart +++ b/mobile/lib/repositories/auth.repository.dart @@ -35,8 +35,10 @@ class AuthRepository extends DatabaseRepository implements IAuthRepository { db.albums.clear(), db.eTags.clear(), db.users.clear(), - _drift.remoteAssetEntity.deleteAll(), _drift.remoteExifEntity.deleteAll(), + _drift.remoteAssetEntity.deleteAll(), + _drift.remoteAlbumEntity.deleteAll(), + _drift.remoteAlbumAssetEntity.deleteAll(), ]); }); }