simplify interface and fix dart test

This commit is contained in:
shenlong-tanwen 2025-05-10 06:21:06 +05:30
parent 95ba4e4d38
commit 57668a8382
14 changed files with 590 additions and 621 deletions

View File

@ -1,6 +1,6 @@
package app.alextran.immich.platform package app.alextran.immich.platform
import Asset import PlatformAsset
import SyncDelta import SyncDelta
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
@ -78,7 +78,7 @@ class MediaManager(context: Context) {
fun getMediaChanges(): SyncDelta { fun getMediaChanges(): SyncDelta {
val genMap = getSavedGenerationMap(ctx) val genMap = getSavedGenerationMap(ctx)
val currentVolumes = MediaStore.getExternalVolumeNames(ctx) val currentVolumes = MediaStore.getExternalVolumeNames(ctx)
val changed = mutableListOf<Asset>() val changed = mutableListOf<PlatformAsset>()
val deleted = mutableListOf<String>() val deleted = mutableListOf<String>()
var hasChanges = genMap.keys != currentVolumes var hasChanges = genMap.keys != currentVolumes
@ -154,7 +154,7 @@ class MediaManager(context: Context) {
val bucketId = cursor.getString(bucketIdColumn) val bucketId = cursor.getString(bucketIdColumn)
changed.add( changed.add(
Asset( PlatformAsset(
id, id,
name, name,
mediaType.toLong(), mediaType.toLong(),

View File

@ -78,7 +78,7 @@ class FlutterError (
) : Throwable() ) : Throwable()
/** Generated class from Pigeon that represents data sent in messages. */ /** Generated class from Pigeon that represents data sent in messages. */
data class Asset ( data class PlatformAsset (
val id: String, val id: String,
val name: String, val name: String,
val type: Long, val type: Long,
@ -89,7 +89,7 @@ data class Asset (
) )
{ {
companion object { companion object {
fun fromList(pigeonVar_list: List<Any?>): Asset { fun fromList(pigeonVar_list: List<Any?>): PlatformAsset {
val id = pigeonVar_list[0] as String val id = pigeonVar_list[0] as String
val name = pigeonVar_list[1] as String val name = pigeonVar_list[1] as String
val type = pigeonVar_list[2] as Long val type = pigeonVar_list[2] as Long
@ -97,7 +97,7 @@ data class Asset (
val updatedAt = pigeonVar_list[4] as Long? val updatedAt = pigeonVar_list[4] as Long?
val durationInSeconds = pigeonVar_list[5] as Long val durationInSeconds = pigeonVar_list[5] as Long
val albumIds = pigeonVar_list[6] as List<String> val albumIds = pigeonVar_list[6] as List<String>
return Asset(id, name, type, createdAt, updatedAt, durationInSeconds, albumIds) return PlatformAsset(id, name, type, createdAt, updatedAt, durationInSeconds, albumIds)
} }
} }
fun toList(): List<Any?> { fun toList(): List<Any?> {
@ -112,7 +112,7 @@ data class Asset (
) )
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (other !is Asset) { if (other !is PlatformAsset) {
return false return false
} }
if (this === other) { if (this === other) {
@ -126,14 +126,14 @@ data class Asset (
/** Generated class from Pigeon that represents data sent in messages. */ /** Generated class from Pigeon that represents data sent in messages. */
data class SyncDelta ( data class SyncDelta (
val hasChanges: Boolean, val hasChanges: Boolean,
val updates: List<Asset>, val updates: List<PlatformAsset>,
val deletes: List<String> val deletes: List<String>
) )
{ {
companion object { companion object {
fun fromList(pigeonVar_list: List<Any?>): SyncDelta { fun fromList(pigeonVar_list: List<Any?>): SyncDelta {
val hasChanges = pigeonVar_list[0] as Boolean val hasChanges = pigeonVar_list[0] as Boolean
val updates = pigeonVar_list[1] as List<Asset> val updates = pigeonVar_list[1] as List<PlatformAsset>
val deletes = pigeonVar_list[2] as List<String> val deletes = pigeonVar_list[2] as List<String>
return SyncDelta(hasChanges, updates, deletes) return SyncDelta(hasChanges, updates, deletes)
} }
@ -161,7 +161,7 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
return when (type) { return when (type) {
129.toByte() -> { 129.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let { return (readValue(buffer) as? List<Any?>)?.let {
Asset.fromList(it) PlatformAsset.fromList(it)
} }
} }
130.toByte() -> { 130.toByte() -> {
@ -174,7 +174,7 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
} }
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
when (value) { when (value) {
is Asset -> { is PlatformAsset -> {
stream.write(129) stream.write(129)
writeValue(stream, value.toList()) writeValue(stream, value.toList())
} }

View File

@ -1,9 +1,9 @@
import Photos import Photos
struct AssetWrapper: Hashable, Equatable { struct AssetWrapper: Hashable, Equatable {
let asset: Asset let asset: PlatformAsset
init(with asset: Asset) { init(with asset: PlatformAsset) {
self.asset = asset self.asset = asset
} }
@ -117,7 +117,7 @@ class MediaManager {
let asset = result.object(at: i) let asset = result.object(at: i)
// Asset wrapper only uses the id for comparison. Multiple change can contain the same asset, skip duplicate changes // Asset wrapper only uses the id for comparison. Multiple change can contain the same asset, skip duplicate changes
let predicate = Asset(id: asset.localIdentifier, name: "", type: 0, createdAt: nil, updatedAt: nil, durationInSeconds: 0, albumIds: []) let predicate = PlatformAsset(id: asset.localIdentifier, name: "", type: 0, createdAt: nil, updatedAt: nil, durationInSeconds: 0, albumIds: [])
if (updatedAssets.contains(AssetWrapper(with: predicate))) { if (updatedAssets.contains(AssetWrapper(with: predicate))) {
continue continue
} }
@ -129,7 +129,7 @@ class MediaManager {
let updatedAt = asset.modificationDate?.timeIntervalSince1970 let updatedAt = asset.modificationDate?.timeIntervalSince1970
let durationInSeconds: Int64 = Int64(asset.duration) let durationInSeconds: Int64 = Int64(asset.duration)
let domainAsset = AssetWrapper(with: Asset( let domainAsset = AssetWrapper(with: PlatformAsset(
id: id, id: id,
name: name, name: name,
type: type, type: type,

View File

@ -129,7 +129,7 @@ func deepHashMessages(value: Any?, hasher: inout Hasher) {
/// Generated class from Pigeon that represents data sent in messages. /// Generated class from Pigeon that represents data sent in messages.
struct Asset: Hashable { struct PlatformAsset: Hashable {
var id: String var id: String
var name: String var name: String
var type: Int64 var type: Int64
@ -140,7 +140,7 @@ struct Asset: Hashable {
// swift-format-ignore: AlwaysUseLowerCamelCase // swift-format-ignore: AlwaysUseLowerCamelCase
static func fromList(_ pigeonVar_list: [Any?]) -> Asset? { static func fromList(_ pigeonVar_list: [Any?]) -> PlatformAsset? {
let id = pigeonVar_list[0] as! String let id = pigeonVar_list[0] as! String
let name = pigeonVar_list[1] as! String let name = pigeonVar_list[1] as! String
let type = pigeonVar_list[2] as! Int64 let type = pigeonVar_list[2] as! Int64
@ -149,7 +149,7 @@ struct Asset: Hashable {
let durationInSeconds = pigeonVar_list[5] as! Int64 let durationInSeconds = pigeonVar_list[5] as! Int64
let albumIds = pigeonVar_list[6] as! [String] let albumIds = pigeonVar_list[6] as! [String]
return Asset( return PlatformAsset(
id: id, id: id,
name: name, name: name,
type: type, type: type,
@ -170,7 +170,7 @@ struct Asset: Hashable {
albumIds, albumIds,
] ]
} }
static func == (lhs: Asset, rhs: Asset) -> Bool { static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
return deepEqualsMessages(lhs.toList(), rhs.toList()) } return deepEqualsMessages(lhs.toList(), rhs.toList()) }
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {
deepHashMessages(value: toList(), hasher: &hasher) deepHashMessages(value: toList(), hasher: &hasher)
@ -180,14 +180,14 @@ struct Asset: Hashable {
/// Generated class from Pigeon that represents data sent in messages. /// Generated class from Pigeon that represents data sent in messages.
struct SyncDelta: Hashable { struct SyncDelta: Hashable {
var hasChanges: Bool var hasChanges: Bool
var updates: [Asset] var updates: [PlatformAsset]
var deletes: [String] var deletes: [String]
// swift-format-ignore: AlwaysUseLowerCamelCase // swift-format-ignore: AlwaysUseLowerCamelCase
static func fromList(_ pigeonVar_list: [Any?]) -> SyncDelta? { static func fromList(_ pigeonVar_list: [Any?]) -> SyncDelta? {
let hasChanges = pigeonVar_list[0] as! Bool let hasChanges = pigeonVar_list[0] as! Bool
let updates = pigeonVar_list[1] as! [Asset] let updates = pigeonVar_list[1] as! [PlatformAsset]
let deletes = pigeonVar_list[2] as! [String] let deletes = pigeonVar_list[2] as! [String]
return SyncDelta( return SyncDelta(
@ -214,7 +214,7 @@ private class MessagesPigeonCodecReader: FlutterStandardReader {
override func readValue(ofType type: UInt8) -> Any? { override func readValue(ofType type: UInt8) -> Any? {
switch type { switch type {
case 129: case 129:
return Asset.fromList(self.readValue() as! [Any?]) return PlatformAsset.fromList(self.readValue() as! [Any?])
case 130: case 130:
return SyncDelta.fromList(self.readValue() as! [Any?]) return SyncDelta.fromList(self.readValue() as! [Any?])
default: default:
@ -225,7 +225,7 @@ private class MessagesPigeonCodecReader: FlutterStandardReader {
private class MessagesPigeonCodecWriter: FlutterStandardWriter { private class MessagesPigeonCodecWriter: FlutterStandardWriter {
override func writeValue(_ value: Any) { override func writeValue(_ value: Any) {
if let value = value as? Asset { if let value = value as? PlatformAsset {
super.writeByte(129) super.writeByte(129)
super.writeValue(value.toList()) super.writeValue(value.toList())
} else if let value = value as? SyncDelta { } else if let value = value as? SyncDelta {

View File

@ -4,27 +4,28 @@ import 'package:immich_mobile/domain/models/local_album.model.dart';
import 'package:immich_mobile/platform/messages.g.dart'; import 'package:immich_mobile/platform/messages.g.dart';
abstract interface class ILocalAlbumRepository implements IDatabaseRepository { abstract interface class ILocalAlbumRepository implements IDatabaseRepository {
Future<void> insert(LocalAlbum album, Iterable<LocalAsset> assets);
Future<void> addAssets(String albumId, Iterable<LocalAsset> assets);
Future<List<LocalAlbum>> getAll({SortLocalAlbumsBy? sortBy}); Future<List<LocalAlbum>> getAll({SortLocalAlbumsBy? sortBy});
Future<List<LocalAsset>> getAssetsForAlbum(String albumId); Future<List<LocalAsset>> getAssetsForAlbum(String albumId);
Future<List<String>> getAssetIdsForAlbum(String albumId); Future<List<String>> getAssetIdsForAlbum(String albumId);
Future<void> update(LocalAlbum album); Future<void> upsert(
LocalAlbum album, {
Iterable<LocalAsset> toUpsert = const [],
Iterable<String> toDelete = const [],
});
Future<void> updateAll(Iterable<LocalAlbum> albums); Future<void> updateAll(Iterable<LocalAlbum> albums);
Future<void> handleSyncDelta(SyncDelta delta);
Future<void> delete(String albumId); Future<void> delete(String albumId);
Future<void> removeMissing(String albumId, Iterable<String> assetIds); Future<void> processDelta(SyncDelta delta);
Future<void> removeAssets(String albumId, Iterable<String> assetIds); Future<void> syncAlbumDeletes(
String albumId,
Iterable<String> assetIdsToKeep,
);
} }
enum SortLocalAlbumsBy { id } enum SortLocalAlbumsBy { id }

View File

@ -43,13 +43,13 @@ class DeviceSyncService {
final deviceAlbums = await _albumMediaRepository.getAll(); final deviceAlbums = await _albumMediaRepository.getAll();
await _localAlbumRepository.updateAll(deviceAlbums); await _localAlbumRepository.updateAll(deviceAlbums);
await _localAlbumRepository.handleSyncDelta(delta); await _localAlbumRepository.processDelta(delta);
if (_platform.isAndroid) { if (_platform.isAndroid) {
final dbAlbums = await _localAlbumRepository.getAll(); final dbAlbums = await _localAlbumRepository.getAll();
for (final album in dbAlbums) { for (final album in dbAlbums) {
final deviceIds = await _hostService.getAssetIdsForAlbum(album.id); final deviceIds = await _hostService.getAssetIdsForAlbum(album.id);
await _localAlbumRepository.removeMissing(album.id, deviceIds); await _localAlbumRepository.syncAlbumDeletes(album.id, deviceIds);
} }
} }
@ -98,7 +98,7 @@ class DeviceSyncService {
? await _albumMediaRepository.getAssetsForAlbum(album.id) ? await _albumMediaRepository.getAssetsForAlbum(album.id)
: <LocalAsset>[]; : <LocalAsset>[];
await _localAlbumRepository.insert(album, assets); await _localAlbumRepository.upsert(album, toUpsert: assets);
_log.fine("Successfully added device album ${album.name}"); _log.fine("Successfully added device album ${album.name}");
} catch (e, s) { } catch (e, s) {
_log.warning("Error while adding device album", e, s); _log.warning("Error while adding device album", e, s);
@ -185,9 +185,9 @@ class DeviceSyncService {
return false; return false;
} }
await _updateAlbum( await _localAlbumRepository.upsert(
deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection), deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection),
assetsToUpsert: newAssets, toUpsert: newAssets,
); );
return true; return true;
@ -213,9 +213,9 @@ class DeviceSyncService {
_log.fine( _log.fine(
"Device album ${deviceAlbum.name} is empty. Removing assets from DB.", "Device album ${deviceAlbum.name} is empty. Removing assets from DB.",
); );
await _updateAlbum( await _localAlbumRepository.upsert(
deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection), deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection),
assetIdsToDelete: assetsInDb.map((a) => a.id), toDelete: assetsInDb.map((a) => a.id),
); );
return true; return true;
} }
@ -228,7 +228,10 @@ class DeviceSyncService {
_log.fine( _log.fine(
"Device album ${deviceAlbum.name} is empty. Adding assets to DB.", "Device album ${deviceAlbum.name} is empty. Adding assets to DB.",
); );
await _updateAlbum(updatedDeviceAlbum, assetsToUpsert: assetsInDevice); await _localAlbumRepository.upsert(
updatedDeviceAlbum,
toUpsert: assetsInDevice,
);
return true; return true;
} }
@ -263,14 +266,14 @@ class DeviceSyncService {
_log.fine( _log.fine(
"No asset changes detected in album ${deviceAlbum.name}. Updating metadata.", "No asset changes detected in album ${deviceAlbum.name}. Updating metadata.",
); );
_localAlbumRepository.update(updatedDeviceAlbum); _localAlbumRepository.upsert(updatedDeviceAlbum);
return true; return true;
} }
await _updateAlbum( await _localAlbumRepository.upsert(
updatedDeviceAlbum, updatedDeviceAlbum,
assetsToUpsert: assetsToUpsert, toUpsert: assetsToUpsert,
assetIdsToDelete: assetsToDelete, toDelete: assetsToDelete,
); );
return true; return true;
@ -280,17 +283,6 @@ class DeviceSyncService {
return true; return true;
} }
Future<void> _updateAlbum(
LocalAlbum album, {
Iterable<LocalAsset> assetsToUpsert = const [],
Iterable<String> assetIdsToDelete = const [],
}) =>
_localAlbumRepository.transaction(() async {
await _localAlbumRepository.addAssets(album.id, assetsToUpsert);
await _localAlbumRepository.update(album);
await _localAlbumRepository.removeAssets(album.id, assetIdsToDelete);
});
bool _assetsEqual(LocalAsset a, LocalAsset b) { bool _assetsEqual(LocalAsset a, LocalAsset b) {
return a.updatedAt.isAtSameMomentAs(b.updatedAt) && return a.updatedAt.isAtSameMomentAs(b.updatedAt) &&
a.createdAt.isAtSameMomentAs(b.createdAt) && a.createdAt.isAtSameMomentAs(b.createdAt) &&

View File

@ -48,7 +48,7 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
if (_platform.isAndroid) { if (_platform.isAndroid) {
e.removeWhere((a) => a.isAll); e.removeWhere((a) => a.isAll);
} }
return e.toDtoList(); return Future.wait(e.map((a) => a.toDto()));
}); });
} }
@ -75,7 +75,7 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
lastPageCount = page.length; lastPageCount = page.length;
pageNumber++; pageNumber++;
} while (lastPageCount == kFetchLocalAssetsBatchSize); } while (lastPageCount == kFetchLocalAssetsBatchSize);
return assets.toDtoList(); return Future.wait(assets.map((a) => a.toDto()));
} }
@override @override
@ -108,11 +108,6 @@ extension on AssetEntity {
); );
} }
extension on List<AssetEntity> {
Future<List<asset.LocalAsset>> toDtoList() =>
Future.wait(map((a) => a.toDto()));
}
extension on AssetPathEntity { extension on AssetPathEntity {
Future<LocalAlbum> toDto({bool withAssetCount = true}) async => LocalAlbum( Future<LocalAlbum> toDto({bool withAssetCount = true}) async => LocalAlbum(
id: id, id: id,
@ -123,8 +118,3 @@ extension on AssetPathEntity {
backupSelection: BackupSelection.none, backupSelection: BackupSelection.none,
); );
} }
extension on List<AssetPathEntity> {
Future<List<LocalAlbum>> toDtoList({bool withAssetCount = true}) =>
Future.wait(map((a) => a.toDto(withAssetCount: withAssetCount)));
}

View File

@ -8,7 +8,7 @@ import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.d
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.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/entities/local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/platform/messages.g.dart' as platform; import 'package:immich_mobile/platform/messages.g.dart';
import 'package:platform/platform.dart'; import 'package:platform/platform.dart';
class DriftLocalAlbumRepository extends DriftDatabaseRepository class DriftLocalAlbumRepository extends DriftDatabaseRepository
@ -52,7 +52,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
// That is not the case on Android since asset <-> album has one:one mapping // That is not the case on Android since asset <-> album has one:one mapping
final assetsToDelete = _platform.isIOS final assetsToDelete = _platform.isIOS
? await _getUniqueAssetsInAlbum(albumId) ? await _getUniqueAssetsInAlbum(albumId)
: await _getAssetsIdsInAlbum(albumId); : await getAssetIdsForAlbum(albumId);
await _deleteAssets(assetsToDelete); await _deleteAssets(assetsToDelete);
// All the other assets that are still associated will be unlinked automatically on-cascade // All the other assets that are still associated will be unlinked automatically on-cascade
@ -62,28 +62,11 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
}); });
@override @override
Future<void> insert(LocalAlbum localAlbum, Iterable<LocalAsset> assets) => Future<void> syncAlbumDeletes(
transaction(() async { String albumId,
await _upsertAssets(assets); Iterable<String> assetIdsToKeep,
// Needs to be after asset upsert to link the thumbnail ) async {
await update(localAlbum); if (assetIdsToKeep.isEmpty) {
await _linkAssetsToAlbum(localAlbum.id, assets);
});
@override
Future<void> addAssets(String albumId, Iterable<LocalAsset> assets) {
if (assets.isEmpty) {
return Future.value();
}
return transaction(() async {
await _upsertAssets(assets);
await _linkAssetsToAlbum(albumId, assets);
});
}
@override
Future<void> removeMissing(String albumId, Iterable<String> assetIds) async {
if (assetIds.isEmpty) {
return Future.value(); return Future.value();
} }
@ -100,45 +83,18 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
]); ]);
subQuery.where( subQuery.where(
_db.localAlbumEntity.id.equals(albumId) & _db.localAlbumEntity.id.equals(albumId) &
_db.localAlbumAssetEntity.assetId.isNotIn(assetIds), _db.localAlbumAssetEntity.assetId.isNotIn(assetIdsToKeep),
); );
return localAsset.id.isInQuery(subQuery); return localAsset.id.isInQuery(subQuery);
}); });
} }
@override @override
Future<void> removeAssets(String albumId, Iterable<String> assetIds) async { Future<void> upsert(
if (assetIds.isEmpty) { LocalAlbum localAlbum, {
return Future.value(); Iterable<LocalAsset> toUpsert = const [],
} Iterable<String> toDelete = const [],
}) {
if (_platform.isAndroid) {
return _deleteAssets(assetIds);
}
final uniqueAssets = await _getUniqueAssetsInAlbum(albumId);
if (uniqueAssets.isEmpty) {
return _unlinkAssetsFromAlbum(albumId, assetIds);
}
// Delete unique assets and unlink others
final uniqueSet = uniqueAssets.toSet();
final assetsToDelete = <String>[];
final assetsToUnLink = <String>[];
for (final assetId in assetIds) {
if (uniqueSet.contains(assetId)) {
assetsToDelete.add(assetId);
} else {
assetsToUnLink.add(assetId);
}
}
return transaction(() async {
await _unlinkAssetsFromAlbum(albumId, assetsToUnLink);
await _deleteAssets(assetsToDelete);
});
}
@override
Future<void> update(LocalAlbum localAlbum) {
final companion = LocalAlbumEntityCompanion.insert( final companion = LocalAlbumEntityCompanion.insert(
id: localAlbum.id, id: localAlbum.id,
name: localAlbum.name, name: localAlbum.name,
@ -146,8 +102,12 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
backupSelection: localAlbum.backupSelection, backupSelection: localAlbum.backupSelection,
); );
return _db.localAlbumEntity return _db.transaction(() async {
await _db.localAlbumEntity
.insertOne(companion, onConflict: DoUpdate((_) => companion)); .insertOne(companion, onConflict: DoUpdate((_) => companion));
await _addAssets(localAlbum.id, toUpsert);
await _removeAssets(localAlbum.id, toDelete);
});
} }
@override @override
@ -193,6 +153,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
subQuery.where(_db.localAlbumEntity.marker_.isNotNull()); subQuery.where(_db.localAlbumEntity.marker_.isNotNull());
return localAsset.id.isInQuery(subQuery); return localAsset.id.isInQuery(subQuery);
}); });
await deleteSmt.go();
} }
await _db.localAlbumEntity.deleteWhere((f) => f.marker_.isNotNull()); await _db.localAlbumEntity.deleteWhere((f) => f.marker_.isNotNull());
@ -227,7 +188,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
} }
@override @override
Future<void> handleSyncDelta(platform.SyncDelta delta) { Future<void> processDelta(SyncDelta delta) {
return _db.transaction(() async { return _db.transaction(() async {
await _deleteAssets(delta.deletes); await _deleteAssets(delta.deletes);
@ -255,15 +216,13 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
}); });
} }
Future<void> _linkAssetsToAlbum( Future<void> _addAssets(String albumId, Iterable<LocalAsset> assets) {
String albumId,
Iterable<LocalAsset> assets,
) {
if (assets.isEmpty) { if (assets.isEmpty) {
return Future.value(); return Future.value();
} }
return transaction(() async {
return _db.localAlbumAssetEntity.insertAll( await _upsertAssets(assets);
await _db.localAlbumAssetEntity.insertAll(
assets.map( assets.map(
(a) => LocalAlbumAssetEntityCompanion.insert( (a) => LocalAlbumAssetEntityCompanion.insert(
assetId: a.id, assetId: a.id,
@ -272,28 +231,49 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
), ),
mode: InsertMode.insertOrIgnore, mode: InsertMode.insertOrIgnore,
); );
});
} }
Future<void> _unlinkAssetsFromAlbum( Future<void> _removeAssets(String albumId, Iterable<String> assetIds) async {
String albumId,
Iterable<String> assetIds,
) {
if (assetIds.isEmpty) { if (assetIds.isEmpty) {
return Future.value(); return Future.value();
} }
return _db.batch( if (_platform.isAndroid) {
return _deleteAssets(assetIds);
}
List<String> assetsToDelete = [];
List<String> assetsToUnLink = [];
final uniqueAssets = await _getUniqueAssetsInAlbum(albumId);
if (uniqueAssets.isEmpty) {
assetsToUnLink = assetIds.toList();
} else {
// Delete unique assets and unlink others
final uniqueSet = uniqueAssets.toSet();
for (final assetId in assetIds) {
if (uniqueSet.contains(assetId)) {
assetsToDelete.add(assetId);
} else {
assetsToUnLink.add(assetId);
}
}
}
return transaction(() async {
if (assetsToUnLink.isNotEmpty) {
await _db.batch(
(batch) => batch.deleteWhere( (batch) => batch.deleteWhere(
_db.localAlbumAssetEntity, _db.localAlbumAssetEntity,
(f) => f.assetId.isIn(assetIds) & f.albumId.equals(albumId), (f) => f.assetId.isIn(assetsToUnLink) & f.albumId.equals(albumId),
), ),
); );
} }
Future<List<String>> _getAssetsIdsInAlbum(String albumId) { await _deleteAssets(assetsToDelete);
final query = _db.localAlbumAssetEntity.select() });
..where((row) => row.albumId.equals(albumId));
return query.map((row) => row.assetId).get();
} }
/// Get all asset ids that are only in this album and not in other albums. /// Get all asset ids that are only in this album and not in other albums.
@ -348,7 +328,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
} }
} }
extension on platform.Asset { extension on PlatformAsset {
LocalAsset toLocalAsset() { LocalAsset toLocalAsset() {
return LocalAsset( return LocalAsset(
id: id, id: id,

View File

@ -12,7 +12,7 @@ import 'package:pigeon/pigeon.dart';
dartOptions: DartOptions(), dartOptions: DartOptions(),
), ),
) )
class Asset { class PlatformAsset {
final String id; final String id;
final String name; final String name;
final int type; // follows AssetType enum from base_asset.model.dart final int type; // follows AssetType enum from base_asset.model.dart
@ -22,7 +22,7 @@ class Asset {
final int durationInSeconds; final int durationInSeconds;
final List<String> albumIds; final List<String> albumIds;
const Asset({ const PlatformAsset({
required this.id, required this.id,
required this.name, required this.name,
required this.type, required this.type,
@ -40,7 +40,7 @@ class SyncDelta {
this.deletes = const [], this.deletes = const [],
}); });
bool hasChanges; bool hasChanges;
List<Asset> updates; List<PlatformAsset> updates;
List<String> deletes; List<String> deletes;
} }

View File

@ -29,8 +29,8 @@ bool _deepEquals(Object? a, Object? b) {
} }
class Asset { class PlatformAsset {
Asset({ PlatformAsset({
required this.id, required this.id,
required this.name, required this.name,
required this.type, required this.type,
@ -69,9 +69,9 @@ class Asset {
Object encode() { Object encode() {
return _toList(); } return _toList(); }
static Asset decode(Object result) { static PlatformAsset decode(Object result) {
result as List<Object?>; result as List<Object?>;
return Asset( return PlatformAsset(
id: result[0]! as String, id: result[0]! as String,
name: result[1]! as String, name: result[1]! as String,
type: result[2]! as int, type: result[2]! as int,
@ -85,7 +85,7 @@ class Asset {
@override @override
// ignore: avoid_equals_and_hash_code_on_mutable_classes // ignore: avoid_equals_and_hash_code_on_mutable_classes
bool operator ==(Object other) { bool operator ==(Object other) {
if (other is! Asset || other.runtimeType != runtimeType) { if (other is! PlatformAsset || other.runtimeType != runtimeType) {
return false; return false;
} }
if (identical(this, other)) { if (identical(this, other)) {
@ -109,7 +109,7 @@ class SyncDelta {
bool hasChanges; bool hasChanges;
List<Asset> updates; List<PlatformAsset> updates;
List<String> deletes; List<String> deletes;
@ -128,7 +128,7 @@ class SyncDelta {
result as List<Object?>; result as List<Object?>;
return SyncDelta( return SyncDelta(
hasChanges: result[0]! as bool, hasChanges: result[0]! as bool,
updates: (result[1] as List<Object?>?)!.cast<Asset>(), updates: (result[1] as List<Object?>?)!.cast<PlatformAsset>(),
deletes: (result[2] as List<Object?>?)!.cast<String>(), deletes: (result[2] as List<Object?>?)!.cast<String>(),
); );
} }
@ -159,7 +159,7 @@ class _PigeonCodec extends StandardMessageCodec {
if (value is int) { if (value is int) {
buffer.putUint8(4); buffer.putUint8(4);
buffer.putInt64(value); buffer.putInt64(value);
} else if (value is Asset) { } else if (value is PlatformAsset) {
buffer.putUint8(129); buffer.putUint8(129);
writeValue(buffer, value.encode()); writeValue(buffer, value.encode());
} else if (value is SyncDelta) { } else if (value is SyncDelta) {
@ -174,7 +174,7 @@ class _PigeonCodec extends StandardMessageCodec {
Object? readValueOfType(int type, ReadBuffer buffer) { Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) { switch (type) {
case 129: case 129:
return Asset.decode(readValue(buffer)!); return PlatformAsset.decode(readValue(buffer)!);
case 130: case 130:
return SyncDelta.decode(readValue(buffer)!); return SyncDelta.decode(readValue(buffer)!);
default: default:

View File

@ -3,6 +3,7 @@ import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart';
import 'package:immich_mobile/platform/messages.g.dart'; import 'package:immich_mobile/platform/messages.g.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:platform/platform.dart';
class MockStoreService extends Mock implements StoreService {} class MockStoreService extends Mock implements StoreService {}
@ -11,3 +12,5 @@ class MockUserService extends Mock implements UserService {}
class MockBackgroundSyncManager extends Mock implements BackgroundSyncManager {} class MockBackgroundSyncManager extends Mock implements BackgroundSyncManager {}
class MockHostService extends Mock implements ImHostService {} class MockHostService extends Mock implements ImHostService {}
class MockPlatform extends Mock implements Platform {}

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@ abstract final class LocalAssetStub {
checksum: "image1-checksum", checksum: "image1-checksum",
type: AssetType.image, type: AssetType.image,
createdAt: DateTime(2019), createdAt: DateTime(2019),
updatedAt: DateTime.now(), updatedAt: DateTime(2020),
width: 1920, width: 1920,
height: 1080, height: 1080,
durationInSeconds: 0, durationInSeconds: 0,

View File

@ -0,0 +1,23 @@
import 'package:immich_mobile/platform/messages.g.dart';
abstract final class PlatformAssetStub {
static PlatformAsset get image1 => PlatformAsset(
id: "asset1",
name: "asset1.jpg",
type: 1,
createdAt: DateTime(2024, 1, 1).millisecondsSinceEpoch,
updatedAt: DateTime(2024, 1, 1).millisecondsSinceEpoch,
durationInSeconds: 0,
albumIds: ["album1"],
);
static PlatformAsset get video1 => PlatformAsset(
id: "asset2",
name: "asset2.mp4",
type: 2,
createdAt: DateTime(2024, 1, 2).millisecondsSinceEpoch,
updatedAt: DateTime(2024, 1, 2).millisecondsSinceEpoch,
durationInSeconds: 120,
albumIds: ["album1"],
);
}