From cc471806fe63f0eafe30d9a869527a4a395ff8ba Mon Sep 17 00:00:00 2001 From: Daimolean <92239625+wuzihao051119@users.noreply.github.com> Date: Mon, 7 Jul 2025 11:01:09 +0800 Subject: [PATCH] feat(mobile): stack sync (#19735) * feat(mobile): stack sync * fix: lint * Update mobile/lib/infrastructure/repositories/sync_api.repository.dart Co-authored-by: Alex --------- Co-authored-by: Alex --- .../drift_schemas/main/drift_schema_v1.json | Bin 42185 -> 27140 bytes mobile/lib/domain/models/stack.model.dart | 84 ++++++++++++++ .../domain/services/sync_stream.service.dart | 19 ++++ .../infrastructure/entities/stack.entity.dart | 22 ++++ .../entities/stack.entity.drift.dart | Bin 0 -> 27587 bytes .../repositories/db.repository.dart | 2 + .../repositories/db.repository.drift.dart | Bin 10615 -> 11199 bytes .../repositories/stack.repository.dart | 30 +++++ .../repositories/sync_api.repository.dart | 7 ++ .../repositories/sync_stream.repository.dart | 106 ++++++++++++------ .../pages/dev/feat_in_development.page.dart | 3 + .../pages/dev/media_stat.page.dart | 4 + mobile/lib/providers/stack.provider.dart | 7 ++ mobile/lib/repositories/auth.repository.dart | 3 + .../services/sync_stream_service_test.dart | 12 ++ 15 files changed, 267 insertions(+), 32 deletions(-) create mode 100644 mobile/lib/domain/models/stack.model.dart create mode 100644 mobile/lib/infrastructure/entities/stack.entity.dart create mode 100644 mobile/lib/infrastructure/entities/stack.entity.drift.dart create mode 100644 mobile/lib/infrastructure/repositories/stack.repository.dart create mode 100644 mobile/lib/providers/stack.provider.dart diff --git a/mobile/drift_schemas/main/drift_schema_v1.json b/mobile/drift_schemas/main/drift_schema_v1.json index 32ba52e366d2ec98891c4f36c09579684794ffaf..493a34cc94c7655f46f133eb7b6358708a62af8b 100644 GIT binary patch literal 27140 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 b!jGTrLSR{TL6CEiwI_ diff --git a/mobile/lib/domain/models/stack.model.dart b/mobile/lib/domain/models/stack.model.dart new file mode 100644 index 000000000..5404eb8f4 --- /dev/null +++ b/mobile/lib/domain/models/stack.model.dart @@ -0,0 +1,84 @@ +import 'dart:convert'; + +// Model for a stack stored in the server +class Stack { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String primaryAssetId; + + const Stack({ + required this.id, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + required this.primaryAssetId, + }); + + Stack copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + String? primaryAssetId, + }) { + return Stack( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + primaryAssetId: primaryAssetId ?? this.primaryAssetId, + ); + } + + Map toMap() { + return { + 'id': id, + 'createdAt': createdAt.millisecondsSinceEpoch, + 'updatedAt': updatedAt.millisecondsSinceEpoch, + 'ownerId': ownerId, + 'primaryAssetId': primaryAssetId, + }; + } + + factory Stack.fromMap(Map map) { + return Stack( + id: map['id'] as String, + createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt'] as int), + updatedAt: DateTime.fromMillisecondsSinceEpoch(map['updatedAt'] as int), + ownerId: map['ownerId'] as String, + primaryAssetId: map['primaryAssetId'] as String, + ); + } + + String toJson() => json.encode(toMap()); + + factory Stack.fromJson(String source) => + Stack.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'Stack(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, ownerId: $ownerId, primaryAssetId: $primaryAssetId)'; + } + + @override + bool operator ==(covariant Stack other) { + if (identical(this, other)) return true; + + return other.id == id && + other.createdAt == createdAt && + other.updatedAt == updatedAt && + other.ownerId == ownerId && + other.primaryAssetId == primaryAssetId; + } + + @override + int get hashCode { + return id.hashCode ^ + createdAt.hashCode ^ + updatedAt.hashCode ^ + ownerId.hashCode ^ + primaryAssetId.hashCode; + } +} diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index c4e40726b..ee0ec6c44 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -154,6 +154,25 @@ class SyncStreamService { return _syncStreamRepository.updateMemoryAssetsV1(data.cast()); case SyncEntityType.memoryToAssetDeleteV1: return _syncStreamRepository.deleteMemoryAssetsV1(data.cast()); + case SyncEntityType.stackV1: + return _syncStreamRepository.updateStacksV1(data.cast()); + case SyncEntityType.stackDeleteV1: + return _syncStreamRepository.deleteStacksV1(data.cast()); + case SyncEntityType.partnerStackV1: + return _syncStreamRepository.updateStacksV1( + data.cast(), + debugLabel: 'partner', + ); + case SyncEntityType.partnerStackBackfillV1: + return _syncStreamRepository.updateStacksV1( + data.cast(), + debugLabel: 'partner backfill', + ); + case SyncEntityType.partnerStackDeleteV1: + return _syncStreamRepository.deleteStacksV1( + data.cast(), + debugLabel: 'partner', + ); default: _logger.warning("Unknown sync data type: $type"); } diff --git a/mobile/lib/infrastructure/entities/stack.entity.dart b/mobile/lib/infrastructure/entities/stack.entity.dart new file mode 100644 index 000000000..92375f19d --- /dev/null +++ b/mobile/lib/infrastructure/entities/stack.entity.dart @@ -0,0 +1,22 @@ +import 'package:drift/drift.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 StackEntity extends Table with DriftDefaultsMixin { + const StackEntity(); + + TextColumn get id => 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 primaryAssetId => text().references(RemoteAssetEntity, #id)(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/stack.entity.drift.dart b/mobile/lib/infrastructure/entities/stack.entity.drift.dart new file mode 100644 index 0000000000000000000000000000000000000000..c0d000e02afd800cd7fd509a6807a627b5ca38eb GIT binary patch literal 27587 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 delta 81 zcmdlV{yk_z73<`ytV)yTunJG+VpC!?-K@(tla)Es&}6bWj}D{hWEUQNaT7h?)S~p% l6vyJ?)DoAX%(N0a4WNq6T|AqZHV5)Q5?bu4%)wO41pqN!8F>Hz diff --git a/mobile/lib/infrastructure/repositories/stack.repository.dart b/mobile/lib/infrastructure/repositories/stack.repository.dart new file mode 100644 index 000000000..7f97f3d9a --- /dev/null +++ b/mobile/lib/infrastructure/repositories/stack.repository.dart @@ -0,0 +1,30 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/stack.model.dart'; +import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +class DriftStackRepository extends DriftDatabaseRepository { + final Drift _db; + const DriftStackRepository(this._db) : super(_db); + + Future> getAll(String userId) { + final query = _db.stackEntity.select() + ..where((e) => e.ownerId.equals(userId)); + + return query.map((stack) { + return stack.toDto(); + }).get(); + } +} + +extension on StackEntityData { + Stack toDto() { + return Stack( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + ownerId: ownerId, + primaryAssetId: primaryAssetId, + ); + } +} diff --git a/mobile/lib/infrastructure/repositories/sync_api.repository.dart b/mobile/lib/infrastructure/repositories/sync_api.repository.dart index 99199a7fc..d43f786a2 100644 --- a/mobile/lib/infrastructure/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -54,6 +54,8 @@ class SyncApiRepository { SyncRequestType.albumToAssetsV1, SyncRequestType.memoriesV1, SyncRequestType.memoryToAssetsV1, + SyncRequestType.stacksV1, + SyncRequestType.partnerStacksV1, ], ).toJson(), ); @@ -163,6 +165,11 @@ const _kResponseMap = { SyncEntityType.memoryDeleteV1: SyncMemoryDeleteV1.fromJson, SyncEntityType.memoryToAssetV1: SyncMemoryAssetV1.fromJson, SyncEntityType.memoryToAssetDeleteV1: SyncMemoryAssetDeleteV1.fromJson, + SyncEntityType.stackV1: SyncStackV1.fromJson, + SyncEntityType.stackDeleteV1: SyncStackDeleteV1.fromJson, + SyncEntityType.partnerStackV1: SyncStackV1.fromJson, + SyncEntityType.partnerStackBackfillV1: SyncStackV1.fromJson, + SyncEntityType.partnerStackDeleteV1: SyncStackDeleteV1.fromJson, }; class _SyncAckV1 { diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index b88083aa0..89f5c2f59 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -12,6 +12,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity. import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/stack.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'; @@ -69,8 +70,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (error, stackTrace) { - _logger.severe('Error: SyncPartnerDeleteV1', error, stackTrace); + } catch (error, stack) { + _logger.severe('Error: SyncPartnerDeleteV1', error, stack); rethrow; } } @@ -92,8 +93,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (error, stackTrace) { - _logger.severe('Error: SyncPartnerV1', error, stackTrace); + } catch (error, stack) { + _logger.severe('Error: SyncPartnerV1', error, stack); rethrow; } } @@ -104,10 +105,10 @@ class SyncStreamRepository extends DriftDatabaseRepository { }) async { try { await _db.remoteAssetEntity.deleteWhere( - (row) => row.id.isIn(data.map((error) => error.assetId)), + (row) => row.id.isIn(data.map((e) => e.assetId)), ); - } catch (error, stackTrace) { - _logger.severe('Error: deleteAssetsV1 - $debugLabel', error, stackTrace); + } catch (error, stack) { + _logger.severe('Error: deleteAssetsV1 - $debugLabel', error, stack); rethrow; } } @@ -142,8 +143,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (error, stackTrace) { - _logger.severe('Error: updateAssetsV1 - $debugLabel', error, stackTrace); + } catch (error, stack) { + _logger.severe('Error: updateAssetsV1 - $debugLabel', error, stack); rethrow; } } @@ -186,11 +187,11 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (error, stackTrace) { + } catch (error, stack) { _logger.severe( 'Error: updateAssetsExifV1 - $debugLabel', error, - stackTrace, + stack, ); rethrow; } @@ -201,8 +202,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { await _db.remoteAlbumEntity.deleteWhere( (row) => row.id.isIn(data.map((e) => e.albumId)), ); - } catch (error, stackTrace) { - _logger.severe('Error: deleteAlbumsV1', error, stackTrace); + } catch (error, stack) { + _logger.severe('Error: deleteAlbumsV1', error, stack); rethrow; } } @@ -229,8 +230,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (error, stackTrace) { - _logger.severe('Error: updateAlbumsV1', error, stackTrace); + } catch (error, stack) { + _logger.severe('Error: updateAlbumsV1', error, stack); rethrow; } } @@ -248,8 +249,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (error, stackTrace) { - _logger.severe('Error: deleteAlbumUsersV1', error, stackTrace); + } catch (error, stack) { + _logger.severe('Error: deleteAlbumUsersV1', error, stack); rethrow; } } @@ -275,11 +276,11 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (error, stackTrace) { + } catch (error, stack) { _logger.severe( 'Error: updateAlbumUsersV1 - $debugLabel', error, - stackTrace, + stack, ); rethrow; } @@ -300,8 +301,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (error, stackTrace) { - _logger.severe('Error: deleteAlbumToAssetsV1', error, stackTrace); + } catch (error, stack) { + _logger.severe('Error: deleteAlbumToAssetsV1', error, stack); rethrow; } } @@ -325,11 +326,11 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (error, stackTrace) { + } catch (error, stack) { _logger.severe( 'Error: updateAlbumToAssetsV1 - $debugLabel', error, - stackTrace, + stack, ); rethrow; } @@ -359,8 +360,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (error, stackTrace) { - _logger.severe('Error: updateMemoriesV1', error, stackTrace); + } catch (error, stack) { + _logger.severe('Error: updateMemoriesV1', error, stack); rethrow; } } @@ -370,8 +371,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { await _db.memoryEntity.deleteWhere( (row) => row.id.isIn(data.map((e) => e.memoryId)), ); - } catch (error, stackTrace) { - _logger.severe('Error: deleteMemoriesV1', error, stackTrace); + } catch (error, stack) { + _logger.severe('Error: deleteMemoriesV1', error, stack); rethrow; } } @@ -392,8 +393,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (error, stackTrace) { - _logger.severe('Error: updateMemoryAssetsV1', error, stackTrace); + } catch (error, stack) { + _logger.severe('Error: updateMemoryAssetsV1', error, stack); rethrow; } } @@ -413,8 +414,49 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (error, stackTrace) { - _logger.severe('Error: deleteMemoryAssetsV1', error, stackTrace); + } catch (error, stack) { + _logger.severe('Error: deleteMemoryAssetsV1', error, stack); + rethrow; + } + } + + Future updateStacksV1( + Iterable data, { + String debugLabel = 'user', + }) async { + try { + await _db.batch((batch) { + for (final stack in data) { + final companion = StackEntityCompanion( + createdAt: Value(stack.createdAt), + updatedAt: Value(stack.updatedAt), + ownerId: Value(stack.ownerId), + primaryAssetId: Value(stack.primaryAssetId), + ); + + batch.insert( + _db.stackEntity, + companion.copyWith(id: Value(stack.id)), + onConflict: DoUpdate((_) => companion), + ); + } + }); + } catch (error, stack) { + _logger.severe('Error: updateStacksV1 - $debugLabel', error, stack); + rethrow; + } + } + + Future deleteStacksV1( + Iterable data, { + String debugLabel = 'user', + }) async { + try { + await _db.stackEntity.deleteWhere( + (row) => row.id.isIn(data.map((e) => e.stackId)), + ); + } catch (error, stack) { + _logger.severe('Error: deleteStacksV1 - $debugLabel', error, stack); rethrow; } } @@ -467,7 +509,7 @@ extension on String { Duration? toDuration() { try { final parts = split(':') - .map((error) => double.parse(error).toInt()) + .map((e) => double.parse(e).toInt()) .toList(growable: false); return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]); 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 f10c042e1..e487d644e 100644 --- a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart +++ b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart @@ -66,6 +66,9 @@ final _features = [ await db.remoteAlbumEntity.deleteAll(); await db.remoteAlbumUserEntity.deleteAll(); await db.remoteAlbumAssetEntity.deleteAll(); + await db.memoryEntity.deleteAll(); + await db.memoryAssetEntity.deleteAll(); + await db.stackEntity.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 f0a648fd5..e5745fa62 100644 --- a/mobile/lib/presentation/pages/dev/media_stat.page.dart +++ b/mobile/lib/presentation/pages/dev/media_stat.page.dart @@ -162,6 +162,10 @@ final _remoteStats = [ name: 'Memories Assets', load: (db) => db.managers.memoryAssetEntity.count(), ), + _Stat( + name: 'Stacks', + load: (db) => db.managers.stackEntity.count(), + ), ]; @RoutePage() diff --git a/mobile/lib/providers/stack.provider.dart b/mobile/lib/providers/stack.provider.dart new file mode 100644 index 000000000..71abd1e87 --- /dev/null +++ b/mobile/lib/providers/stack.provider.dart @@ -0,0 +1,7 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/infrastructure/repositories/stack.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; + +final driftStackProvider = Provider( + (ref) => DriftStackRepository(ref.watch(driftProvider)), +); diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart index 3ad8e3458..4ee4d8c13 100644 --- a/mobile/lib/repositories/auth.repository.dart +++ b/mobile/lib/repositories/auth.repository.dart @@ -40,6 +40,9 @@ class AuthRepository extends DatabaseRepository { _drift.remoteAlbumEntity.deleteAll(), _drift.remoteAlbumAssetEntity.deleteAll(), _drift.remoteAlbumUserEntity.deleteAll(), + _drift.memoryEntity.deleteAll(), + _drift.memoryAssetEntity.deleteAll(), + _drift.stackEntity.deleteAll(), ]); }); } diff --git a/mobile/test/domain/services/sync_stream_service_test.dart b/mobile/test/domain/services/sync_stream_service_test.dart index 28288422f..27cd8c5b2 100644 --- a/mobile/test/domain/services/sync_stream_service_test.dart +++ b/mobile/test/domain/services/sync_stream_service_test.dart @@ -89,6 +89,18 @@ void main() { .thenAnswer(successHandler); when(() => mockSyncStreamRepo.deleteMemoryAssetsV1(any())) .thenAnswer(successHandler); + when( + () => mockSyncStreamRepo.updateStacksV1( + any(), + debugLabel: any(named: 'debugLabel'), + ), + ).thenAnswer(successHandler); + when( + () => mockSyncStreamRepo.deleteStacksV1( + any(), + debugLabel: any(named: 'debugLabel'), + ), + ).thenAnswer(successHandler); sut = SyncStreamService( syncApiRepository: mockSyncApiRepo,