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 clearSyncCheckpoint()
|
||||||
fun getAssetIdsForAlbum(albumId: String): List<String>
|
fun getAssetIdsForAlbum(albumId: String): List<String>
|
||||||
fun getAlbums(): List<ImAlbum>
|
fun getAlbums(): List<ImAlbum>
|
||||||
|
fun getAssetsCountSince(albumId: String, timestamp: Long): Long
|
||||||
|
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<ImAsset>
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** The codec used by NativeSyncApi. */
|
/** The codec used by NativeSyncApi. */
|
||||||
@ -350,6 +352,42 @@ interface NativeSyncApi {
|
|||||||
channel.setMessageHandler(null)
|
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
|
// 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 {
|
override fun getMediaChanges(): SyncDelta {
|
||||||
throw IllegalStateException("Method not supported on this Android version.")
|
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 {
|
override fun getMediaChanges(): SyncDelta {
|
||||||
val genMap = getSavedGenerationMap()
|
val genMap = getSavedGenerationMap()
|
||||||
val currentVolumes = MediaStore.getExternalVolumeNames(ctx)
|
val currentVolumes = MediaStore.getExternalVolumeNames(ctx)
|
||||||
@ -78,7 +72,7 @@ class NativeSyncApiImpl30(context: Context) : NativeSyncApiImplBase(context), Na
|
|||||||
storedGen.toString()
|
storedGen.toString()
|
||||||
)
|
)
|
||||||
|
|
||||||
getAssets(volume, selection, selectionArgs).forEach {
|
getAssets(getCursor(volume, selection, selectionArgs)).forEach {
|
||||||
when (it) {
|
when (it) {
|
||||||
is AssetResult.ValidAsset -> {
|
is AssetResult.ValidAsset -> {
|
||||||
changed.add(it.asset)
|
changed.add(it.asset)
|
||||||
|
@ -2,141 +2,176 @@ package app.alextran.immich.sync
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.database.Cursor
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
sealed class AssetResult {
|
sealed class AssetResult {
|
||||||
data class ValidAsset(val asset: ImAsset, val albumId: String) : AssetResult()
|
data class ValidAsset(val asset: ImAsset, val albumId: String) : AssetResult()
|
||||||
data class InvalidAsset(val assetId: String) : AssetResult()
|
data class InvalidAsset(val assetId: String) : AssetResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
open class NativeSyncApiImplBase(context: Context) {
|
open class NativeSyncApiImplBase(context: Context) {
|
||||||
private val ctx: Context = context.applicationContext
|
private val ctx: Context = context.applicationContext
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val MEDIA_SELECTION =
|
const val MEDIA_SELECTION =
|
||||||
"(${MediaStore.Files.FileColumns.MEDIA_TYPE} = ? OR ${MediaStore.Files.FileColumns.MEDIA_TYPE} = ?)"
|
"(${MediaStore.Files.FileColumns.MEDIA_TYPE} = ? OR ${MediaStore.Files.FileColumns.MEDIA_TYPE} = ?)"
|
||||||
val MEDIA_SELECTION_ARGS = arrayOf(
|
val MEDIA_SELECTION_ARGS = arrayOf(
|
||||||
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString(),
|
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString(),
|
||||||
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString()
|
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString()
|
||||||
)
|
)
|
||||||
}
|
const val BUCKET_SELECTION = "(${MediaStore.Files.FileColumns.BUCKET_ID} = ?)"
|
||||||
|
val ASSET_PROJECTION = arrayOf(
|
||||||
|
MediaStore.MediaColumns._ID,
|
||||||
|
MediaStore.MediaColumns.DATA,
|
||||||
|
MediaStore.MediaColumns.DISPLAY_NAME,
|
||||||
|
MediaStore.MediaColumns.DATE_TAKEN,
|
||||||
|
MediaStore.MediaColumns.DATE_ADDED,
|
||||||
|
MediaStore.MediaColumns.DATE_MODIFIED,
|
||||||
|
MediaStore.Files.FileColumns.MEDIA_TYPE,
|
||||||
|
MediaStore.MediaColumns.BUCKET_ID,
|
||||||
|
MediaStore.MediaColumns.DURATION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
protected fun getAssets(
|
protected fun getCursor(
|
||||||
volume: String,
|
volume: String,
|
||||||
selection: String,
|
selection: String,
|
||||||
selectionArgs: Array<String>,
|
selectionArgs: Array<String>,
|
||||||
): Sequence<AssetResult> {
|
projection: Array<String> = ASSET_PROJECTION,
|
||||||
val projection = arrayOf(
|
sortOrder: String? = null
|
||||||
MediaStore.MediaColumns._ID,
|
): Cursor? = ctx.contentResolver.query(
|
||||||
MediaStore.MediaColumns.DATA,
|
MediaStore.Files.getContentUri(volume),
|
||||||
MediaStore.MediaColumns.DISPLAY_NAME,
|
projection,
|
||||||
MediaStore.MediaColumns.DATE_TAKEN,
|
selection,
|
||||||
MediaStore.MediaColumns.DATE_ADDED,
|
selectionArgs,
|
||||||
MediaStore.MediaColumns.DATE_MODIFIED,
|
sortOrder,
|
||||||
MediaStore.Files.FileColumns.MEDIA_TYPE,
|
)
|
||||||
MediaStore.MediaColumns.BUCKET_ID,
|
|
||||||
MediaStore.MediaColumns.DURATION
|
|
||||||
)
|
|
||||||
|
|
||||||
return sequence {
|
protected fun getAssets(cursor: Cursor?): Sequence<AssetResult> {
|
||||||
ctx.contentResolver.query(
|
return sequence {
|
||||||
MediaStore.Files.getContentUri(volume),
|
cursor?.use { c ->
|
||||||
projection,
|
val idColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
|
||||||
selection,
|
val dataColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
|
||||||
selectionArgs,
|
val nameColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)
|
||||||
null
|
val dateTakenColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_TAKEN)
|
||||||
)?.use { cursor ->
|
val dateAddedColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED)
|
||||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
|
val dateModifiedColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)
|
||||||
val dataColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
|
val mediaTypeColumn = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE)
|
||||||
val nameColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)
|
val bucketIdColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.BUCKET_ID)
|
||||||
val dateTakenColumn =
|
val durationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION)
|
||||||
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)
|
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (c.moveToNext()) {
|
||||||
val id = cursor.getLong(idColumn).toString()
|
val id = c.getLong(idColumn).toString()
|
||||||
val path = cursor.getString(dataColumn)
|
|
||||||
if (path.isNullOrBlank() || !File(path).exists()) {
|
|
||||||
yield(AssetResult.InvalidAsset(id))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
val mediaType = cursor.getInt(mediaTypeColumn)
|
val path = c.getString(dataColumn)
|
||||||
val name = cursor.getString(nameColumn)
|
if (path.isNullOrBlank() || !File(path).exists()) {
|
||||||
// Date taken is milliseconds since epoch, Date added is seconds since epoch
|
yield(AssetResult.InvalidAsset(id))
|
||||||
val createdAt = (cursor.getLong(dateTakenColumn).takeIf { it > 0 }?.div(1000))
|
continue
|
||||||
?: cursor.getLong(dateAddedColumn)
|
}
|
||||||
// Date modified is seconds since epoch
|
|
||||||
val modifiedAt = cursor.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)
|
|
||||||
|
|
||||||
yield(
|
val mediaType = c.getInt(mediaTypeColumn)
|
||||||
AssetResult.ValidAsset(
|
val name = c.getString(nameColumn)
|
||||||
ImAsset(id, name, mediaType.toLong(), createdAt, modifiedAt, duration),
|
// Date taken is milliseconds since epoch, Date added is seconds since epoch
|
||||||
bucketId
|
val createdAt = (c.getLong(dateTakenColumn).takeIf { it > 0 }?.div(1000))
|
||||||
)
|
?: c.getLong(dateAddedColumn)
|
||||||
)
|
// Date modified is seconds since epoch
|
||||||
}
|
val modifiedAt = c.getLong(dateModifiedColumn)
|
||||||
}
|
// Duration is milliseconds
|
||||||
|
val duration = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0
|
||||||
|
else c.getLong(durationColumn) / 1000
|
||||||
|
val bucketId = c.getString(bucketIdColumn)
|
||||||
|
|
||||||
|
val asset = ImAsset(id, name, mediaType.toLong(), createdAt, modifiedAt, duration)
|
||||||
|
yield(AssetResult.ValidAsset(asset, bucketId))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
fun getAlbums(): List<ImAlbum> {
|
||||||
fun getAlbums(): List<ImAlbum> {
|
val albums = mutableListOf<ImAlbum>()
|
||||||
val albums = mutableListOf<ImAlbum>()
|
val albumsCount = mutableMapOf<String, Int>()
|
||||||
val albumsCount = mutableMapOf<String, Int>()
|
|
||||||
|
|
||||||
val projection = arrayOf(
|
val projection = arrayOf(
|
||||||
MediaStore.Files.FileColumns.BUCKET_ID,
|
MediaStore.Files.FileColumns.BUCKET_ID,
|
||||||
MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME,
|
MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME,
|
||||||
MediaStore.Files.FileColumns.DATE_MODIFIED,
|
MediaStore.Files.FileColumns.DATE_MODIFIED,
|
||||||
)
|
)
|
||||||
val selection =
|
val selection =
|
||||||
"(${MediaStore.Files.FileColumns.BUCKET_ID} IS NOT NULL) AND $MEDIA_SELECTION"
|
"(${MediaStore.Files.FileColumns.BUCKET_ID} IS NOT NULL) AND $MEDIA_SELECTION"
|
||||||
|
|
||||||
ctx.contentResolver.query(
|
getCursor(
|
||||||
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
|
MediaStore.VOLUME_EXTERNAL,
|
||||||
projection,
|
selection,
|
||||||
selection,
|
MEDIA_SELECTION_ARGS,
|
||||||
MEDIA_SELECTION_ARGS,
|
projection,
|
||||||
"${MediaStore.Files.FileColumns.DATE_MODIFIED} DESC"
|
"${MediaStore.Files.FileColumns.DATE_MODIFIED} DESC"
|
||||||
)?.use { cursor ->
|
)?.use { cursor ->
|
||||||
val bucketIdColumn =
|
val bucketIdColumn =
|
||||||
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.BUCKET_ID)
|
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.BUCKET_ID)
|
||||||
val bucketNameColumn =
|
val bucketNameColumn =
|
||||||
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME)
|
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME)
|
||||||
val dateModified =
|
val dateModified =
|
||||||
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_MODIFIED)
|
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_MODIFIED)
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
val id = cursor.getString(bucketIdColumn)
|
val id = cursor.getString(bucketIdColumn)
|
||||||
val count = albumsCount.getOrDefault(id, 0)
|
|
||||||
if (count != 0) {
|
|
||||||
albumsCount[id] = count + 1
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
val name = cursor.getString(bucketNameColumn)
|
val count = albumsCount.getOrDefault(id, 0)
|
||||||
val updatedAt = cursor.getLong(dateModified)
|
if (count != 0) {
|
||||||
albums.add(ImAlbum(id, name, updatedAt, false, 0))
|
albumsCount[id] = count + 1
|
||||||
albumsCount[id] = 1
|
continue
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return albums.map { album ->
|
val name = cursor.getString(bucketNameColumn)
|
||||||
val count = albumsCount[album.id] ?: 0
|
val updatedAt = cursor.getLong(dateModified)
|
||||||
album.copy(assetCount = count.toLong())
|
albums.add(ImAlbum(id, name, updatedAt, false, 0))
|
||||||
}.sortedBy { it.id }
|
albumsCount[id] = 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 clearSyncCheckpoint() throws
|
||||||
func getAssetIdsForAlbum(albumId: String) throws -> [String]
|
func getAssetIdsForAlbum(albumId: String) throws -> [String]
|
||||||
func getAlbums() throws -> [ImAlbum]
|
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`.
|
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||||
@ -404,5 +406,41 @@ class NativeSyncApiSetup {
|
|||||||
} else {
|
} else {
|
||||||
getAlbumsChannel.setMessageHandler(nil)
|
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
|
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/interfaces/db.interface.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.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/domain/models/local_album.model.dart';
|
||||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
|
||||||
|
|
||||||
abstract interface class ILocalAlbumRepository implements IDatabaseRepository {
|
abstract interface class ILocalAlbumRepository implements IDatabaseRepository {
|
||||||
Future<List<LocalAlbum>> getAll({SortLocalAlbumsBy? sortBy});
|
Future<List<LocalAlbum>> getAll({SortLocalAlbumsBy? sortBy});
|
||||||
@ -20,7 +19,11 @@ abstract interface class ILocalAlbumRepository implements IDatabaseRepository {
|
|||||||
|
|
||||||
Future<void> delete(String albumId);
|
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(
|
Future<void> syncAlbumDeletes(
|
||||||
String albumId,
|
String albumId,
|
||||||
|
@ -2,7 +2,6 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/widgets.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/interfaces/local_album.interface.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.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/domain/models/local_album.model.dart';
|
||||||
@ -15,7 +14,6 @@ import 'package:logging/logging.dart';
|
|||||||
import 'package:platform/platform.dart';
|
import 'package:platform/platform.dart';
|
||||||
|
|
||||||
class DeviceSyncService {
|
class DeviceSyncService {
|
||||||
final IAlbumMediaRepository _albumMediaRepository;
|
|
||||||
final ILocalAlbumRepository _localAlbumRepository;
|
final ILocalAlbumRepository _localAlbumRepository;
|
||||||
final NativeSyncApi _nativeSyncApi;
|
final NativeSyncApi _nativeSyncApi;
|
||||||
final Platform _platform;
|
final Platform _platform;
|
||||||
@ -23,13 +21,11 @@ class DeviceSyncService {
|
|||||||
final Logger _log = Logger("DeviceSyncService");
|
final Logger _log = Logger("DeviceSyncService");
|
||||||
|
|
||||||
DeviceSyncService({
|
DeviceSyncService({
|
||||||
required IAlbumMediaRepository albumMediaRepository,
|
|
||||||
required ILocalAlbumRepository localAlbumRepository,
|
required ILocalAlbumRepository localAlbumRepository,
|
||||||
required NativeSyncApi nativeSyncApi,
|
required NativeSyncApi nativeSyncApi,
|
||||||
required StoreService storeService,
|
required StoreService storeService,
|
||||||
Platform? platform,
|
Platform? platform,
|
||||||
}) : _albumMediaRepository = albumMediaRepository,
|
}) : _localAlbumRepository = localAlbumRepository,
|
||||||
_localAlbumRepository = localAlbumRepository,
|
|
||||||
_nativeSyncApi = nativeSyncApi,
|
_nativeSyncApi = nativeSyncApi,
|
||||||
_storeService = storeService,
|
_storeService = storeService,
|
||||||
_platform = platform ?? const LocalPlatform();
|
_platform = platform ?? const LocalPlatform();
|
||||||
@ -58,7 +54,11 @@ class DeviceSyncService {
|
|||||||
|
|
||||||
final deviceAlbums = await _nativeSyncApi.getAlbums();
|
final deviceAlbums = await _nativeSyncApi.getAlbums();
|
||||||
await _localAlbumRepository.updateAll(deviceAlbums.toLocalAlbums());
|
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();
|
final dbAlbums = await _localAlbumRepository.getAll();
|
||||||
// On Android, we need to sync all albums since it is not possible to
|
// 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}");
|
_log.fine("Adding device album ${album.name}");
|
||||||
|
|
||||||
final assets = album.assetCount > 0
|
final assets = album.assetCount > 0
|
||||||
? await _albumMediaRepository.getAssetsForAlbum(album.id)
|
? await _nativeSyncApi.getAssetsForAlbum(album.id)
|
||||||
: <LocalAsset>[];
|
: <ImAsset>[];
|
||||||
|
|
||||||
await _localAlbumRepository.upsert(album, toUpsert: assets);
|
await _localAlbumRepository.upsert(
|
||||||
|
album,
|
||||||
|
toUpsert: assets.toLocalAssets(),
|
||||||
|
);
|
||||||
_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);
|
||||||
@ -198,17 +201,13 @@ class DeviceSyncService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all assets that are modified after the last known modifiedTime
|
final updatedTime =
|
||||||
final newAssets = await _albumMediaRepository.getAssetsForAlbum(
|
(dbAlbum.updatedAt.millisecondsSinceEpoch ~/ 1000) + 1;
|
||||||
deviceAlbum.id,
|
final newAssetsCount =
|
||||||
updateTimeCond: DateTimeFilter(
|
await _nativeSyncApi.getAssetsCountSince(deviceAlbum.id, updatedTime);
|
||||||
min: dbAlbum.updatedAt.add(const Duration(seconds: 1)),
|
|
||||||
max: deviceAlbum.updatedAt,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Early return if no new assets were found
|
// Early return if no new assets were found
|
||||||
if (newAssets.isEmpty) {
|
if (newAssetsCount == 0) {
|
||||||
_log.fine(
|
_log.fine(
|
||||||
"No new assets found despite album having changes. Proceeding to full sync for ${dbAlbum.name}",
|
"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
|
// 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");
|
_log.fine("Local album has modifications. Proceeding to full sync");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final newAssets = await _nativeSyncApi.getAssetsForAlbum(
|
||||||
|
deviceAlbum.id,
|
||||||
|
updatedTimeCond: updatedTime,
|
||||||
|
);
|
||||||
|
|
||||||
await _localAlbumRepository.upsert(
|
await _localAlbumRepository.upsert(
|
||||||
deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection),
|
deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection),
|
||||||
toUpsert: newAssets,
|
toUpsert: newAssets.toLocalAssets(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -239,7 +243,9 @@ class DeviceSyncService {
|
|||||||
Future<bool> fullDiff(LocalAlbum dbAlbum, LocalAlbum deviceAlbum) async {
|
Future<bool> fullDiff(LocalAlbum dbAlbum, LocalAlbum deviceAlbum) async {
|
||||||
try {
|
try {
|
||||||
final assetsInDevice = deviceAlbum.assetCount > 0
|
final assetsInDevice = deviceAlbum.assetCount > 0
|
||||||
? await _albumMediaRepository.getAssetsForAlbum(deviceAlbum.id)
|
? await _nativeSyncApi
|
||||||
|
.getAssetsForAlbum(deviceAlbum.id)
|
||||||
|
.then((a) => a.toLocalAssets())
|
||||||
: <LocalAsset>[];
|
: <LocalAsset>[];
|
||||||
final assetsInDb = dbAlbum.assetCount > 0
|
final assetsInDb = dbAlbum.assetCount > 0
|
||||||
? await _localAlbumRepository.getAssetsForAlbum(dbAlbum.id)
|
? await _localAlbumRepository.getAssetsForAlbum(dbAlbum.id)
|
||||||
@ -348,3 +354,22 @@ extension on Iterable<ImAlbum> {
|
|||||||
).toList();
|
).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_album_asset.entity.drift.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/native_sync_api.g.dart';
|
|
||||||
import 'package:platform/platform.dart';
|
import 'package:platform/platform.dart';
|
||||||
|
|
||||||
class DriftLocalAlbumRepository extends DriftDatabaseRepository
|
class DriftLocalAlbumRepository extends DriftDatabaseRepository
|
||||||
@ -187,19 +186,21 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@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 {
|
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
|
// The ugly casting below is required for now because the generated code
|
||||||
// casts the returned values from the platform during decoding them
|
// casts the returned values from the platform during decoding them
|
||||||
// and iterating over them causes the type to be List<Object?> instead of
|
// and iterating over them causes the type to be List<Object?> instead of
|
||||||
// List<String>
|
// List<String>
|
||||||
await _db.batch((batch) async {
|
await _db.batch((batch) async {
|
||||||
delta.assetAlbums
|
assetAlbums.cast<String, List<Object?>>().forEach((assetId, albumIds) {
|
||||||
.cast<String, List<Object?>>()
|
|
||||||
.forEach((assetId, albumIds) {
|
|
||||||
batch.deleteWhere(
|
batch.deleteWhere(
|
||||||
_db.localAlbumAssetEntity,
|
_db.localAlbumAssetEntity,
|
||||||
(f) =>
|
(f) =>
|
||||||
@ -209,9 +210,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
await _db.batch((batch) async {
|
await _db.batch((batch) async {
|
||||||
delta.assetAlbums
|
assetAlbums.cast<String, List<Object?>>().forEach((assetId, albumIds) {
|
||||||
.cast<String, List<Object?>>()
|
|
||||||
.forEach((assetId, albumIds) {
|
|
||||||
batch.insertAll(
|
batch.insertAll(
|
||||||
_db.localAlbumAssetEntity,
|
_db.localAlbumAssetEntity,
|
||||||
albumIds.cast<String?>().nonNulls.map(
|
albumIds.cast<String?>().nonNulls.map(
|
||||||
@ -339,24 +338,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on ImAsset {
|
extension on LocalAlbumEntityData {
|
||||||
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 {
|
|
||||||
LocalAlbum toDto({int assetCount = 0}) {
|
LocalAlbum toDto({int assetCount = 0}) {
|
||||||
return LocalAlbum(
|
return LocalAlbum(
|
||||||
id: id,
|
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>();
|
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: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/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/infrastructure/repositories/local_album.repository.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
|
|
||||||
final albumMediaRepositoryProvider =
|
|
||||||
Provider<IAlbumMediaRepository>((ref) => const AlbumMediaRepository());
|
|
||||||
|
|
||||||
final localAlbumRepository = Provider<ILocalAlbumRepository>(
|
final localAlbumRepository = Provider<ILocalAlbumRepository>(
|
||||||
(ref) => DriftLocalAlbumRepository(ref.watch(driftProvider)),
|
(ref) => DriftLocalAlbumRepository(ref.watch(driftProvider)),
|
||||||
);
|
);
|
||||||
|
@ -28,7 +28,6 @@ final syncStreamRepositoryProvider = Provider(
|
|||||||
|
|
||||||
final deviceSyncServiceProvider = Provider(
|
final deviceSyncServiceProvider = Provider(
|
||||||
(ref) => DeviceSyncService(
|
(ref) => DeviceSyncService(
|
||||||
albumMediaRepository: ref.watch(albumMediaRepositoryProvider),
|
|
||||||
localAlbumRepository: ref.watch(localAlbumRepository),
|
localAlbumRepository: ref.watch(localAlbumRepository),
|
||||||
nativeSyncApi: ref.watch(nativeSyncApiProvider),
|
nativeSyncApi: ref.watch(nativeSyncApiProvider),
|
||||||
storeService: ref.watch(storeServiceProvider),
|
storeService: ref.watch(storeServiceProvider),
|
||||||
|
@ -80,4 +80,10 @@ abstract class NativeSyncApi {
|
|||||||
|
|
||||||
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
|
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
|
||||||
List<ImAlbum> getAlbums();
|
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