remove photo_manager dep for sync

This commit is contained in:
shenlong-tanwen 2025-05-24 00:58:55 +05:30
parent a41a156ce4
commit 5ea136cd32
15 changed files with 381 additions and 273 deletions

View File

@ -245,6 +245,8 @@ interface NativeSyncApi {
fun clearSyncCheckpoint()
fun getAssetIdsForAlbum(albumId: String): List<String>
fun getAlbums(): List<ImAlbum>
fun getAssetsCountSince(albumId: String, timestamp: Long): Long
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<ImAsset>
companion object {
/** The codec used by NativeSyncApi. */
@ -350,6 +352,42 @@ interface NativeSyncApi {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince$separatedMessageChannelSuffix", codec, taskQueue)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val albumIdArg = args[0] as String
val timestampArg = args[1] as Long
val wrapped: List<Any?> = try {
listOf(api.getAssetsCountSince(albumIdArg, timestampArg))
} catch (exception: Throwable) {
MessagesPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$separatedMessageChannelSuffix", codec, taskQueue)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val albumIdArg = args[0] as String
val updatedTimeCondArg = args[1] as Long?
val wrapped: List<Any?> = try {
listOf(api.getAssetsForAlbum(albumIdArg, updatedTimeCondArg))
} catch (exception: Throwable) {
MessagesPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}

View File

@ -18,10 +18,6 @@ class NativeSyncApiImpl26(context: Context) : NativeSyncApiImplBase(context), Na
// No-op for Android 10 and below
}
override fun getAssetIdsForAlbum(albumId: String): List<String> {
throw IllegalStateException("Method not supported on this Android version.")
}
override fun getMediaChanges(): SyncDelta {
throw IllegalStateException("Method not supported on this Android version.")
}

View File

@ -47,12 +47,6 @@ class NativeSyncApiImpl30(context: Context) : NativeSyncApiImplBase(context), Na
}
}
override fun getAssetIdsForAlbum(albumId: String): List<String> = getAssets(
MediaStore.VOLUME_EXTERNAL,
"${MediaStore.Files.FileColumns.BUCKET_ID} = ? AND $MEDIA_SELECTION",
arrayOf(albumId, *MEDIA_SELECTION_ARGS)
).mapNotNull { (it as? AssetResult.ValidAsset)?.asset?.id }.toList()
override fun getMediaChanges(): SyncDelta {
val genMap = getSavedGenerationMap()
val currentVolumes = MediaStore.getExternalVolumeNames(ctx)
@ -78,7 +72,7 @@ class NativeSyncApiImpl30(context: Context) : NativeSyncApiImplBase(context), Na
storedGen.toString()
)
getAssets(volume, selection, selectionArgs).forEach {
getAssets(getCursor(volume, selection, selectionArgs)).forEach {
when (it) {
is AssetResult.ValidAsset -> {
changed.add(it.asset)

View File

@ -2,6 +2,7 @@ package app.alextran.immich.sync
import android.annotation.SuppressLint
import android.content.Context
import android.database.Cursor
import android.provider.MediaStore
import java.io.File
@ -10,6 +11,7 @@ sealed class AssetResult {
data class InvalidAsset(val assetId: String) : AssetResult()
}
@SuppressLint("InlinedApi")
open class NativeSyncApiImplBase(context: Context) {
private val ctx: Context = context.applicationContext
@ -20,14 +22,8 @@ open class NativeSyncApiImplBase(context: Context) {
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString(),
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString()
)
}
protected fun getAssets(
volume: String,
selection: String,
selectionArgs: Array<String>,
): Sequence<AssetResult> {
val projection = arrayOf(
const val BUCKET_SELECTION = "(${MediaStore.Files.FileColumns.BUCKET_ID} = ?)"
val ASSET_PROJECTION = arrayOf(
MediaStore.MediaColumns._ID,
MediaStore.MediaColumns.DATA,
MediaStore.MediaColumns.DISPLAY_NAME,
@ -38,61 +34,63 @@ open class NativeSyncApiImplBase(context: Context) {
MediaStore.MediaColumns.BUCKET_ID,
MediaStore.MediaColumns.DURATION
)
}
return sequence {
ctx.contentResolver.query(
protected fun getCursor(
volume: String,
selection: String,
selectionArgs: Array<String>,
projection: Array<String> = ASSET_PROJECTION,
sortOrder: String? = null
): Cursor? = ctx.contentResolver.query(
MediaStore.Files.getContentUri(volume),
projection,
selection,
selectionArgs,
null
)?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
val dataColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
val nameColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)
val dateTakenColumn =
cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_TAKEN)
val dateAddedColumn =
cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED)
val dateModifiedColumn =
cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)
val mediaTypeColumn =
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE)
val bucketIdColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.BUCKET_ID)
val durationColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION)
sortOrder,
)
while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn).toString()
val path = cursor.getString(dataColumn)
protected fun getAssets(cursor: Cursor?): Sequence<AssetResult> {
return sequence {
cursor?.use { c ->
val idColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
val dataColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
val nameColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)
val dateTakenColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_TAKEN)
val dateAddedColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED)
val dateModifiedColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)
val mediaTypeColumn = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE)
val bucketIdColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.BUCKET_ID)
val durationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION)
while (c.moveToNext()) {
val id = c.getLong(idColumn).toString()
val path = c.getString(dataColumn)
if (path.isNullOrBlank() || !File(path).exists()) {
yield(AssetResult.InvalidAsset(id))
continue
}
val mediaType = cursor.getInt(mediaTypeColumn)
val name = cursor.getString(nameColumn)
val mediaType = c.getInt(mediaTypeColumn)
val name = c.getString(nameColumn)
// Date taken is milliseconds since epoch, Date added is seconds since epoch
val createdAt = (cursor.getLong(dateTakenColumn).takeIf { it > 0 }?.div(1000))
?: cursor.getLong(dateAddedColumn)
val createdAt = (c.getLong(dateTakenColumn).takeIf { it > 0 }?.div(1000))
?: c.getLong(dateAddedColumn)
// Date modified is seconds since epoch
val modifiedAt = cursor.getLong(dateModifiedColumn)
val modifiedAt = c.getLong(dateModifiedColumn)
// Duration is milliseconds
val duration = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0
else cursor.getLong(durationColumn) / 1000
val bucketId = cursor.getString(bucketIdColumn)
else c.getLong(durationColumn) / 1000
val bucketId = c.getString(bucketIdColumn)
yield(
AssetResult.ValidAsset(
ImAsset(id, name, mediaType.toLong(), createdAt, modifiedAt, duration),
bucketId
)
)
val asset = ImAsset(id, name, mediaType.toLong(), createdAt, modifiedAt, duration)
yield(AssetResult.ValidAsset(asset, bucketId))
}
}
}
}
@SuppressLint("InlinedApi")
fun getAlbums(): List<ImAlbum> {
val albums = mutableListOf<ImAlbum>()
val albumsCount = mutableMapOf<String, Int>()
@ -105,11 +103,11 @@ open class NativeSyncApiImplBase(context: Context) {
val selection =
"(${MediaStore.Files.FileColumns.BUCKET_ID} IS NOT NULL) AND $MEDIA_SELECTION"
ctx.contentResolver.query(
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
projection,
getCursor(
MediaStore.VOLUME_EXTERNAL,
selection,
MEDIA_SELECTION_ARGS,
projection,
"${MediaStore.Files.FileColumns.DATE_MODIFIED} DESC"
)?.use { cursor ->
val bucketIdColumn =
@ -121,6 +119,7 @@ open class NativeSyncApiImplBase(context: Context) {
while (cursor.moveToNext()) {
val id = cursor.getString(bucketIdColumn)
val count = albumsCount.getOrDefault(id, 0)
if (count != 0) {
albumsCount[id] = count + 1
@ -134,9 +133,45 @@ open class NativeSyncApiImplBase(context: Context) {
}
}
return albums.map { album ->
val count = albumsCount[album.id] ?: 0
album.copy(assetCount = count.toLong())
}.sortedBy { it.id }
return albums.map { it.copy(assetCount = albumsCount[it.id]?.toLong() ?: 0) }
.sortedBy { it.id }
}
fun getAssetIdsForAlbum(albumId: String): List<String> {
val projection = arrayOf(MediaStore.MediaColumns._ID)
return getCursor(
MediaStore.VOLUME_EXTERNAL,
"$BUCKET_SELECTION AND $MEDIA_SELECTION",
arrayOf(albumId, *MEDIA_SELECTION_ARGS),
projection
)?.use { cursor ->
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
generateSequence {
if (cursor.moveToNext()) cursor.getLong(idColumn).toString() else null
}.toList()
} ?: emptyList()
}
fun getAssetsCountSince(albumId: String, timestamp: Long): Long =
getCursor(
MediaStore.VOLUME_EXTERNAL,
"$BUCKET_SELECTION AND ${MediaStore.Files.FileColumns.DATE_ADDED} > ? AND $MEDIA_SELECTION",
arrayOf(albumId, timestamp.toString(), *MEDIA_SELECTION_ARGS),
)?.use { cursor -> cursor.count.toLong() } ?: 0L
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<ImAsset> {
var selection = "$BUCKET_SELECTION AND $MEDIA_SELECTION"
val selectionArgs = mutableListOf(albumId, *MEDIA_SELECTION_ARGS)
if (updatedTimeCond != null) {
selection += " AND (${MediaStore.Files.FileColumns.DATE_MODIFIED} > ? OR ${MediaStore.Files.FileColumns.DATE_ADDED} > ?)"
selectionArgs.addAll(listOf(updatedTimeCond.toString(), updatedTimeCond.toString()))
}
return getAssets(getCursor(MediaStore.VOLUME_EXTERNAL, selection, selectionArgs.toTypedArray()))
.mapNotNull { result -> (result as? AssetResult.ValidAsset)?.asset }
.toList()
}
}

View File

@ -305,6 +305,8 @@ protocol NativeSyncApi {
func clearSyncCheckpoint() throws
func getAssetIdsForAlbum(albumId: String) throws -> [String]
func getAlbums() throws -> [ImAlbum]
func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [ImAsset]
}
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@ -404,5 +406,41 @@ class NativeSyncApiSetup {
} else {
getAlbumsChannel.setMessageHandler(nil)
}
let getAssetsCountSinceChannel = taskQueue == nil
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
if let api = api {
getAssetsCountSinceChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let albumIdArg = args[0] as! String
let timestampArg = args[1] as! Int64
do {
let result = try api.getAssetsCountSince(albumId: albumIdArg, timestamp: timestampArg)
reply(wrapResult(result))
} catch {
reply(wrapError(error))
}
}
} else {
getAssetsCountSinceChannel.setMessageHandler(nil)
}
let getAssetsForAlbumChannel = taskQueue == nil
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
if let api = api {
getAssetsForAlbumChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let albumIdArg = args[0] as! String
let updatedTimeCondArg: Int64? = nilOrValue(args[1])
do {
let result = try api.getAssetsForAlbum(albumId: albumIdArg, updatedTimeCond: updatedTimeCondArg)
reply(wrapResult(result))
} catch {
reply(wrapError(error))
}
}
} else {
getAssetsForAlbumChannel.setMessageHandler(nil)
}
}
}

View File

@ -199,4 +199,36 @@ class NativeSyncApiImpl: NativeSyncApi {
}
return ids
}
func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 {
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
guard let album = collections.firstObject else {
return 0
}
let date = NSDate(timeIntervalSince1970: TimeInterval(timestamp))
let options = PHFetchOptions()
options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date)
let assets = PHAsset.fetchAssets(in: album, options: options)
return Int64(assets.count)
}
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [ImAsset] {
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
guard let album = collections.firstObject else {
return []
}
let options = PHFetchOptions()
if(updatedTimeCond != nil) {
let date = NSDate(timeIntervalSince1970: TimeInterval(updatedTimeCond!))
options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date)
}
let result = PHAsset.fetchAssets(in: album, options: options)
var assets: [ImAsset] = []
result.enumerateObjects { (asset, _, _) in
assets.append(asset.toImAsset())
}
return assets
}
}

View File

@ -1,15 +0,0 @@
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
abstract interface class IAlbumMediaRepository {
Future<List<LocalAsset>> getAssetsForAlbum(
String albumId, {
DateTimeFilter? updateTimeCond,
});
}
class DateTimeFilter {
final DateTime min;
final DateTime max;
const DateTimeFilter({required this.min, required this.max});
}

View File

@ -1,7 +1,6 @@
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/local_album.model.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
abstract interface class ILocalAlbumRepository implements IDatabaseRepository {
Future<List<LocalAlbum>> getAll({SortLocalAlbumsBy? sortBy});
@ -20,7 +19,11 @@ abstract interface class ILocalAlbumRepository implements IDatabaseRepository {
Future<void> delete(String albumId);
Future<void> processDelta(SyncDelta delta);
Future<void> processDelta({
required List<LocalAsset> updates,
required List<String> deletes,
required Map<String, List<String>> assetAlbums,
});
Future<void> syncAlbumDeletes(
String albumId,

View File

@ -2,7 +2,6 @@ import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package:immich_mobile/domain/interfaces/album_media.interface.dart';
import 'package:immich_mobile/domain/interfaces/local_album.interface.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/local_album.model.dart';
@ -15,7 +14,6 @@ import 'package:logging/logging.dart';
import 'package:platform/platform.dart';
class DeviceSyncService {
final IAlbumMediaRepository _albumMediaRepository;
final ILocalAlbumRepository _localAlbumRepository;
final NativeSyncApi _nativeSyncApi;
final Platform _platform;
@ -23,13 +21,11 @@ class DeviceSyncService {
final Logger _log = Logger("DeviceSyncService");
DeviceSyncService({
required IAlbumMediaRepository albumMediaRepository,
required ILocalAlbumRepository localAlbumRepository,
required NativeSyncApi nativeSyncApi,
required StoreService storeService,
Platform? platform,
}) : _albumMediaRepository = albumMediaRepository,
_localAlbumRepository = localAlbumRepository,
}) : _localAlbumRepository = localAlbumRepository,
_nativeSyncApi = nativeSyncApi,
_storeService = storeService,
_platform = platform ?? const LocalPlatform();
@ -58,7 +54,11 @@ class DeviceSyncService {
final deviceAlbums = await _nativeSyncApi.getAlbums();
await _localAlbumRepository.updateAll(deviceAlbums.toLocalAlbums());
await _localAlbumRepository.processDelta(delta);
await _localAlbumRepository.processDelta(
updates: delta.updates.toLocalAssets(),
deletes: delta.deletes,
assetAlbums: delta.assetAlbums,
);
final dbAlbums = await _localAlbumRepository.getAll();
// On Android, we need to sync all albums since it is not possible to
@ -135,10 +135,13 @@ class DeviceSyncService {
_log.fine("Adding device album ${album.name}");
final assets = album.assetCount > 0
? await _albumMediaRepository.getAssetsForAlbum(album.id)
: <LocalAsset>[];
? await _nativeSyncApi.getAssetsForAlbum(album.id)
: <ImAsset>[];
await _localAlbumRepository.upsert(album, toUpsert: assets);
await _localAlbumRepository.upsert(
album,
toUpsert: assets.toLocalAssets(),
);
_log.fine("Successfully added device album ${album.name}");
} catch (e, s) {
_log.warning("Error while adding device album", e, s);
@ -198,17 +201,13 @@ class DeviceSyncService {
return false;
}
// Get all assets that are modified after the last known modifiedTime
final newAssets = await _albumMediaRepository.getAssetsForAlbum(
deviceAlbum.id,
updateTimeCond: DateTimeFilter(
min: dbAlbum.updatedAt.add(const Duration(seconds: 1)),
max: deviceAlbum.updatedAt,
),
);
final updatedTime =
(dbAlbum.updatedAt.millisecondsSinceEpoch ~/ 1000) + 1;
final newAssetsCount =
await _nativeSyncApi.getAssetsCountSince(deviceAlbum.id, updatedTime);
// Early return if no new assets were found
if (newAssets.isEmpty) {
if (newAssetsCount == 0) {
_log.fine(
"No new assets found despite album having changes. Proceeding to full sync for ${dbAlbum.name}",
);
@ -216,14 +215,19 @@ class DeviceSyncService {
}
// Check whether there is only addition or if there has been deletions
if (deviceAlbum.assetCount != dbAlbum.assetCount + newAssets.length) {
if (deviceAlbum.assetCount != dbAlbum.assetCount + newAssetsCount) {
_log.fine("Local album has modifications. Proceeding to full sync");
return false;
}
final newAssets = await _nativeSyncApi.getAssetsForAlbum(
deviceAlbum.id,
updatedTimeCond: updatedTime,
);
await _localAlbumRepository.upsert(
deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection),
toUpsert: newAssets,
toUpsert: newAssets.toLocalAssets(),
);
return true;
@ -239,7 +243,9 @@ class DeviceSyncService {
Future<bool> fullDiff(LocalAlbum dbAlbum, LocalAlbum deviceAlbum) async {
try {
final assetsInDevice = deviceAlbum.assetCount > 0
? await _albumMediaRepository.getAssetsForAlbum(deviceAlbum.id)
? await _nativeSyncApi
.getAssetsForAlbum(deviceAlbum.id)
.then((a) => a.toLocalAssets())
: <LocalAsset>[];
final assetsInDb = dbAlbum.assetCount > 0
? await _localAlbumRepository.getAssetsForAlbum(dbAlbum.id)
@ -348,3 +354,22 @@ extension on Iterable<ImAlbum> {
).toList();
}
}
extension on Iterable<ImAsset> {
List<LocalAsset> toLocalAssets() {
return map(
(e) => LocalAsset(
id: e.id,
name: e.name,
type: AssetType.values.elementAtOrNull(e.type) ?? AssetType.other,
createdAt: e.createdAt == null
? DateTime.now()
: DateTime.fromMillisecondsSinceEpoch(e.createdAt! * 1000),
updatedAt: e.updatedAt == null
? DateTime.now()
: DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000),
durationInSeconds: e.durationInSeconds,
),
).toList();
}
}

View File

@ -1,76 +0,0 @@
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/interfaces/album_media.interface.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'
as asset;
import 'package:photo_manager/photo_manager.dart';
class AlbumMediaRepository implements IAlbumMediaRepository {
const AlbumMediaRepository();
PMFilter _getAlbumFilter({
DateTimeFilter? createdTimeCond,
DateTimeFilter? updateTimeCond,
}) =>
FilterOptionGroup(
imageOption: const FilterOption(
// needTitle is expected to be slow on iOS but is required to fetch the asset title
needTitle: true,
sizeConstraint: SizeConstraint(ignoreSize: true),
),
videoOption: const FilterOption(
needTitle: true,
sizeConstraint: SizeConstraint(ignoreSize: true),
durationConstraint: DurationConstraint(allowNullable: true),
),
// This is needed to get the modified time of the album
containsPathModified: true,
createTimeCond: createdTimeCond == null
? DateTimeCond.def().copyWith(ignore: true)
: DateTimeCond(min: createdTimeCond.min, max: createdTimeCond.max),
updateTimeCond: updateTimeCond == null
? DateTimeCond.def().copyWith(ignore: true)
: DateTimeCond(min: updateTimeCond.min, max: updateTimeCond.max),
orders: [],
);
@override
Future<List<asset.LocalAsset>> getAssetsForAlbum(
String albumId, {
DateTimeFilter? updateTimeCond,
}) async {
final assetPathEntity = await AssetPathEntity.obtainPathFromProperties(
id: albumId,
optionGroup: _getAlbumFilter(updateTimeCond: updateTimeCond),
);
final assets = <AssetEntity>[];
int pageNumber = 0, lastPageCount = 0;
do {
final page = await assetPathEntity.getAssetListPaged(
page: pageNumber,
size: kFetchLocalAssetsBatchSize,
);
assets.addAll(page);
lastPageCount = page.length;
pageNumber++;
} while (lastPageCount == kFetchLocalAssetsBatchSize);
return Future.wait(assets.map((a) => a.toDto()));
}
}
extension on AssetEntity {
Future<asset.LocalAsset> toDto() async => asset.LocalAsset(
id: id,
name: title ?? await titleAsync,
type: switch (type) {
AssetType.other => asset.AssetType.other,
AssetType.image => asset.AssetType.image,
AssetType.video => asset.AssetType.video,
AssetType.audio => asset.AssetType.audio,
},
createdAt: createDateTime,
updatedAt: modifiedDateTime,
width: width,
height: height,
durationInSeconds: duration,
);
}

View File

@ -6,7 +6,6 @@ import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.d
import 'package:immich_mobile/infrastructure/entities/local_album_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/platform/native_sync_api.g.dart';
import 'package:platform/platform.dart';
class DriftLocalAlbumRepository extends DriftDatabaseRepository
@ -187,19 +186,21 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
}
@override
Future<void> processDelta(SyncDelta delta) {
Future<void> processDelta({
required List<LocalAsset> updates,
required List<String> deletes,
required Map<String, List<String>> assetAlbums,
}) {
return _db.transaction(() async {
await _deleteAssets(delta.deletes);
await _deleteAssets(deletes);
await _upsertAssets(delta.updates.map((a) => a.toLocalAsset()));
await _upsertAssets(updates);
// The ugly casting below is required for now because the generated code
// casts the returned values from the platform during decoding them
// and iterating over them causes the type to be List<Object?> instead of
// List<String>
await _db.batch((batch) async {
delta.assetAlbums
.cast<String, List<Object?>>()
.forEach((assetId, albumIds) {
assetAlbums.cast<String, List<Object?>>().forEach((assetId, albumIds) {
batch.deleteWhere(
_db.localAlbumAssetEntity,
(f) =>
@ -209,9 +210,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
});
});
await _db.batch((batch) async {
delta.assetAlbums
.cast<String, List<Object?>>()
.forEach((assetId, albumIds) {
assetAlbums.cast<String, List<Object?>>().forEach((assetId, albumIds) {
batch.insertAll(
_db.localAlbumAssetEntity,
albumIds.cast<String?>().nonNulls.map(
@ -339,24 +338,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
}
}
extension on ImAsset {
LocalAsset toLocalAsset() {
return LocalAsset(
id: id,
name: name,
type: AssetType.values.elementAtOrNull(type) ?? AssetType.other,
createdAt: createdAt == null
? DateTime.now()
: DateTime.fromMillisecondsSinceEpoch(createdAt! * 1000),
updatedAt: updatedAt == null
? DateTime.now()
: DateTime.fromMillisecondsSinceEpoch(updatedAt! * 1000),
durationInSeconds: durationInSeconds,
);
}
}
extension LocalAlbumEntityX on LocalAlbumEntityData {
extension on LocalAlbumEntityData {
LocalAlbum toDto({int assetCount = 0}) {
return LocalAlbum(
id: id,

View File

@ -419,4 +419,60 @@ class NativeSyncApi {
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<ImAlbum>();
}
}
Future<int> getAssetsCountSince(String albumId, int timestamp) async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[albumId, timestamp]);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as int?)!;
}
}
Future<List<ImAsset>> getAssetsForAlbum(String albumId, {int? updatedTimeCond}) async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[albumId, updatedTimeCond]);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<ImAsset>();
}
}
}

View File

@ -1,13 +1,8 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/album_media.interface.dart';
import 'package:immich_mobile/domain/interfaces/local_album.interface.dart';
import 'package:immich_mobile/infrastructure/repositories/album_media.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
final albumMediaRepositoryProvider =
Provider<IAlbumMediaRepository>((ref) => const AlbumMediaRepository());
final localAlbumRepository = Provider<ILocalAlbumRepository>(
(ref) => DriftLocalAlbumRepository(ref.watch(driftProvider)),
);

View File

@ -28,7 +28,6 @@ final syncStreamRepositoryProvider = Provider(
final deviceSyncServiceProvider = Provider(
(ref) => DeviceSyncService(
albumMediaRepository: ref.watch(albumMediaRepositoryProvider),
localAlbumRepository: ref.watch(localAlbumRepository),
nativeSyncApi: ref.watch(nativeSyncApiProvider),
storeService: ref.watch(storeServiceProvider),

View File

@ -80,4 +80,10 @@ abstract class NativeSyncApi {
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
List<ImAlbum> getAlbums();
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
int getAssetsCountSince(String albumId, int timestamp);
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
List<ImAsset> getAssetsForAlbum(String albumId, {int? updatedTimeCond});
}