mirror of
https://github.com/immich-app/immich.git
synced 2025-06-01 12:44:17 -04:00
simplify interface and fix dart test
This commit is contained in:
parent
95ba4e4d38
commit
57668a8382
@ -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(),
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
@ -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 }
|
||||||
|
@ -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) &&
|
||||||
|
@ -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)));
|
|
||||||
}
|
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
18
mobile/lib/platform/messages.g.dart
generated
18
mobile/lib/platform/messages.g.dart
generated
@ -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:
|
||||||
|
@ -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
2
mobile/test/fixtures/local_asset.stub.dart
vendored
2
mobile/test/fixtures/local_asset.stub.dart
vendored
@ -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,
|
||||||
|
23
mobile/test/fixtures/platform_asset.stub.dart
vendored
Normal file
23
mobile/test/fixtures/platform_asset.stub.dart
vendored
Normal 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"],
|
||||||
|
);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user