feat: memories sync (#19644)

* feat: memories sync

* Update mobile/lib/infrastructure/repositories/sync_stream.repository.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update mobile/lib/infrastructure/repositories/sync_stream.repository.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* show sync information

* tests and pr feedback

* pr feedback

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Alex
2025-07-02 14:18:37 -05:00
committed by GitHub
parent 7855974a29
commit 445f9174ea
16 changed files with 3491 additions and 33 deletions
@@ -7,6 +7,8 @@ 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/memory.entity.dart';
import 'package:immich_mobile/infrastructure/entities/memory_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';
@@ -46,6 +48,8 @@ class IsarDatabaseRepository implements IDatabaseRepository {
RemoteAlbumEntity,
RemoteAlbumAssetEntity,
RemoteAlbumUserEntity,
MemoryEntity,
MemoryAssetEntity,
],
include: {
'package:immich_mobile/infrastructure/entities/merged_asset.drift',
@@ -23,9 +23,13 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.
as i10;
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'
as i11;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
as i12;
import 'package:drift/internal/modular.dart' as i13;
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
as i13;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
as i14;
import 'package:drift/internal/modular.dart' as i15;
abstract class $Drift extends i0.GeneratedDatabase {
$Drift(i0.QueryExecutor e) : super(e);
@@ -51,8 +55,11 @@ abstract class $Drift extends i0.GeneratedDatabase {
i10.$RemoteAlbumAssetEntityTable(this);
late final i11.$RemoteAlbumUserEntityTable remoteAlbumUserEntity =
i11.$RemoteAlbumUserEntityTable(this);
i12.MergedAssetDrift get mergedAssetDrift => i13.ReadDatabaseContainer(this)
.accessor<i12.MergedAssetDrift>(i12.MergedAssetDrift.new);
late final i12.$MemoryEntityTable memoryEntity = i12.$MemoryEntityTable(this);
late final i13.$MemoryAssetEntityTable memoryAssetEntity =
i13.$MemoryAssetEntityTable(this);
i14.MergedAssetDrift get mergedAssetDrift => i15.ReadDatabaseContainer(this)
.accessor<i14.MergedAssetDrift>(i14.MergedAssetDrift.new);
@override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@@ -71,7 +78,9 @@ abstract class $Drift extends i0.GeneratedDatabase {
remoteExifEntity,
remoteAlbumEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity
remoteAlbumUserEntity,
memoryEntity,
memoryAssetEntity
];
@override
i0.StreamQueryUpdateRules get streamUpdateRules =>
@@ -175,6 +184,27 @@ abstract class $Drift extends i0.GeneratedDatabase {
kind: i0.UpdateKind.delete),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('user_entity',
limitUpdateKind: i0.UpdateKind.delete),
result: [
i0.TableUpdate('memory_entity', kind: i0.UpdateKind.delete),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('remote_asset_entity',
limitUpdateKind: i0.UpdateKind.delete),
result: [
i0.TableUpdate('memory_asset_entity', kind: i0.UpdateKind.delete),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('memory_entity',
limitUpdateKind: i0.UpdateKind.delete),
result: [
i0.TableUpdate('memory_asset_entity', kind: i0.UpdateKind.delete),
],
),
],
);
@override
@@ -208,4 +238,8 @@ class $DriftManager {
_db, _db.remoteAlbumAssetEntity);
i11.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i11
.$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity);
i12.$$MemoryEntityTableTableManager get memoryEntity =>
i12.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
i13.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
i13.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
}
@@ -0,0 +1,37 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/memory.model.dart';
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
class DriftMemoryRepository extends DriftDatabaseRepository {
final Drift _db;
const DriftMemoryRepository(this._db) : super(_db);
Future<List<Memory>> getAll(String userId) {
final query = _db.memoryEntity.select()
..where((e) => e.ownerId.equals(userId));
return query.map((memory) {
return memory.toDto();
}).get();
}
}
extension on MemoryEntityData {
Memory toDto() {
return Memory(
id: id,
createdAt: createdAt,
updatedAt: updatedAt,
deletedAt: deletedAt,
ownerId: ownerId,
type: type,
data: MemoryData.fromJson(data),
isSaved: isSaved,
memoryAt: memoryAt,
seenAt: seenAt,
showAt: showAt,
hideAt: hideAt,
);
}
}
@@ -52,6 +52,8 @@ class SyncApiRepository {
SyncRequestType.albumAssetsV1,
SyncRequestType.albumAssetExifsV1,
SyncRequestType.albumToAssetsV1,
SyncRequestType.memoriesV1,
SyncRequestType.memoryToAssetsV1,
],
).toJson(),
);
@@ -157,6 +159,10 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
SyncEntityType.albumToAssetBackfillV1: SyncAlbumToAssetV1.fromJson,
SyncEntityType.albumToAssetDeleteV1: SyncAlbumToAssetDeleteV1.fromJson,
SyncEntityType.syncAckV1: _SyncAckV1.fromJson,
SyncEntityType.memoryV1: SyncMemoryV1.fromJson,
SyncEntityType.memoryDeleteV1: SyncMemoryDeleteV1.fromJson,
SyncEntityType.memoryToAssetV1: SyncMemoryAssetV1.fromJson,
SyncEntityType.memoryToAssetDeleteV1: SyncMemoryAssetDeleteV1.fromJson,
};
class _SyncAckV1 {
@@ -1,12 +1,17 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/memory.model.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/partner.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_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/user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:logging/logging.dart';
@@ -64,8 +69,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (e, s) {
_logger.severe('Error: SyncPartnerDeleteV1', e, s);
} catch (error, stackTrace) {
_logger.severe('Error: SyncPartnerDeleteV1', error, stackTrace);
rethrow;
}
}
@@ -87,8 +92,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (e, s) {
_logger.severe('Error: SyncPartnerV1', e, s);
} catch (error, stackTrace) {
_logger.severe('Error: SyncPartnerV1', error, stackTrace);
rethrow;
}
}
@@ -98,10 +103,11 @@ class SyncStreamRepository extends DriftDatabaseRepository {
String debugLabel = 'user',
}) async {
try {
await _db.remoteAssetEntity
.deleteWhere((row) => row.id.isIn(data.map((e) => e.assetId)));
} catch (e, s) {
_logger.severe('Error: deleteAssetsV1 - $debugLabel', e, s);
await _db.remoteAssetEntity.deleteWhere(
(row) => row.id.isIn(data.map((error) => error.assetId)),
);
} catch (error, stackTrace) {
_logger.severe('Error: deleteAssetsV1 - $debugLabel', error, stackTrace);
rethrow;
}
}
@@ -136,8 +142,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (e, s) {
_logger.severe('Error: updateAssetsV1 - $debugLabel', e, s);
} catch (error, stackTrace) {
_logger.severe('Error: updateAssetsV1 - $debugLabel', error, stackTrace);
rethrow;
}
}
@@ -180,18 +186,23 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (e, s) {
_logger.severe('Error: updateAssetsExifV1 - $debugLabel', e, s);
} catch (error, stackTrace) {
_logger.severe(
'Error: updateAssetsExifV1 - $debugLabel',
error,
stackTrace,
);
rethrow;
}
}
Future<void> deleteAlbumsV1(Iterable<SyncAlbumDeleteV1> data) async {
try {
await _db.remoteAlbumEntity
.deleteWhere((row) => row.id.isIn(data.map((e) => e.albumId)));
} catch (e, s) {
_logger.severe('Error: deleteAlbumsV1', e, s);
await _db.remoteAlbumEntity.deleteWhere(
(row) => row.id.isIn(data.map((e) => e.albumId)),
);
} catch (error, stackTrace) {
_logger.severe('Error: deleteAlbumsV1', error, stackTrace);
rethrow;
}
}
@@ -218,8 +229,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (e, s) {
_logger.severe('Error: updateAlbumsV1', e, s);
} catch (error, stackTrace) {
_logger.severe('Error: updateAlbumsV1', error, stackTrace);
rethrow;
}
}
@@ -237,8 +248,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (e, s) {
_logger.severe('Error: deleteAlbumUsersV1', e, s);
} catch (error, stackTrace) {
_logger.severe('Error: deleteAlbumUsersV1', error, stackTrace);
rethrow;
}
}
@@ -264,8 +275,12 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (e, s) {
_logger.severe('Error: updateAlbumUsersV1 - $debugLabel', e, s);
} catch (error, stackTrace) {
_logger.severe(
'Error: updateAlbumUsersV1 - $debugLabel',
error,
stackTrace,
);
rethrow;
}
}
@@ -285,8 +300,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (e, s) {
_logger.severe('Error: deleteAlbumToAssetsV1', e, s);
} catch (error, stackTrace) {
_logger.severe('Error: deleteAlbumToAssetsV1', error, stackTrace);
rethrow;
}
}
@@ -310,8 +325,96 @@ class SyncStreamRepository extends DriftDatabaseRepository {
);
}
});
} catch (e, s) {
_logger.severe('Error: updateAlbumToAssetsV1 - $debugLabel', e, s);
} catch (error, stackTrace) {
_logger.severe(
'Error: updateAlbumToAssetsV1 - $debugLabel',
error,
stackTrace,
);
rethrow;
}
}
Future<void> updateMemoriesV1(Iterable<SyncMemoryV1> data) async {
try {
await _db.batch((batch) {
for (final memory in data) {
final companion = MemoryEntityCompanion(
createdAt: Value(memory.createdAt),
deletedAt: Value(memory.deletedAt),
ownerId: Value(memory.ownerId),
type: Value(memory.type.toMemoryType()),
data: Value(jsonEncode(memory.data)),
isSaved: Value(memory.isSaved),
memoryAt: Value(memory.memoryAt),
seenAt: Value.absentIfNull(memory.seenAt),
showAt: Value.absentIfNull(memory.showAt),
hideAt: Value.absentIfNull(memory.hideAt),
);
batch.insert(
_db.memoryEntity,
companion.copyWith(id: Value(memory.id)),
onConflict: DoUpdate((_) => companion),
);
}
});
} catch (error, stackTrace) {
_logger.severe('Error: updateMemoriesV1', error, stackTrace);
rethrow;
}
}
Future<void> deleteMemoriesV1(Iterable<SyncMemoryDeleteV1> data) async {
try {
await _db.memoryEntity.deleteWhere(
(row) => row.id.isIn(data.map((e) => e.memoryId)),
);
} catch (error, stackTrace) {
_logger.severe('Error: deleteMemoriesV1', error, stackTrace);
rethrow;
}
}
Future<void> updateMemoryAssetsV1(Iterable<SyncMemoryAssetV1> data) async {
try {
await _db.batch((batch) {
for (final asset in data) {
final companion = MemoryAssetEntityCompanion(
memoryId: Value(asset.memoryId),
assetId: Value(asset.assetId),
);
batch.insert(
_db.memoryAssetEntity,
companion,
onConflict: DoNothing(),
);
}
});
} catch (error, stackTrace) {
_logger.severe('Error: updateMemoryAssetsV1', error, stackTrace);
rethrow;
}
}
Future<void> deleteMemoryAssetsV1(
Iterable<SyncMemoryAssetDeleteV1> data,
) async {
try {
await _db.batch((batch) {
for (final asset in data) {
batch.delete(
_db.memoryAssetEntity,
MemoryAssetEntityCompanion(
memoryId: Value(asset.memoryId),
assetId: Value(asset.assetId),
),
);
}
});
} catch (error, stackTrace) {
_logger.severe('Error: deleteMemoryAssetsV1', error, stackTrace);
rethrow;
}
}
@@ -335,6 +438,13 @@ extension on AssetOrder {
};
}
extension on MemoryType {
MemoryTypeEnum toMemoryType() => switch (this) {
MemoryType.onThisDay => MemoryTypeEnum.onThisDay,
_ => throw Exception('Unknown MemoryType value: $this'),
};
}
extension on api.AlbumUserRole {
AlbumUserRole toAlbumUserRole() => switch (this) {
api.AlbumUserRole.editor => AlbumUserRole.editor,
@@ -357,7 +467,7 @@ extension on String {
Duration? toDuration() {
try {
final parts = split(':')
.map((e) => double.parse(e).toInt())
.map((error) => double.parse(error).toInt())
.toList(growable: false);
return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]);