mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
remove photo_manager dep for sync
This commit is contained in:
parent
a41a156ce4
commit
5ea136cd32
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.")
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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});
|
||||
}
|
@ -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,
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
@ -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,
|
||||
|
56
mobile/lib/platform/native_sync_api.g.dart
generated
56
mobile/lib/platform/native_sync_api.g.dart
generated
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)),
|
||||
);
|
||||
|
@ -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),
|
||||
|
@ -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});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user