mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	make the native calls synchronous, pass datetime as long
This commit is contained in:
		
							parent
							
								
									87599daaac
								
							
						
					
					
						commit
						63ebba671b
					
				@ -9,9 +9,6 @@ import androidx.annotation.RequiresApi
 | 
			
		||||
import androidx.annotation.RequiresExtension
 | 
			
		||||
import kotlinx.serialization.json.Json
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.time.Instant
 | 
			
		||||
import java.time.ZoneOffset
 | 
			
		||||
import java.time.format.DateTimeFormatter
 | 
			
		||||
 | 
			
		||||
class MediaManager(context: Context) {
 | 
			
		||||
  private val ctx: Context = context.applicationContext
 | 
			
		||||
@ -30,30 +27,25 @@ class MediaManager(context: Context) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @RequiresApi(Build.VERSION_CODES.Q)
 | 
			
		||||
  fun shouldFullSync(callback: (Result<Boolean>) -> Unit) {
 | 
			
		||||
  fun shouldFullSync(): Boolean {
 | 
			
		||||
    val prefs = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
 | 
			
		||||
    val currVersion = MediaStore.getVersion(ctx)
 | 
			
		||||
    val lastVersion = prefs.getString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, null)
 | 
			
		||||
    callback(Result.success(currVersion != lastVersion))
 | 
			
		||||
    return MediaStore.getVersion(ctx) != prefs.getString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, null)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @RequiresApi(Build.VERSION_CODES.Q)
 | 
			
		||||
  @RequiresExtension(extension = Build.VERSION_CODES.R, version = 1)
 | 
			
		||||
  fun checkpointSync(callback: (Result<Unit>) -> Unit) {
 | 
			
		||||
  fun checkpointSync() {
 | 
			
		||||
    val genMap =
 | 
			
		||||
      MediaStore.getExternalVolumeNames(ctx).associateWith { MediaStore.getGeneration(ctx, it) }
 | 
			
		||||
    val prefs = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
 | 
			
		||||
    prefs.edit().apply {
 | 
			
		||||
    ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE).edit().apply {
 | 
			
		||||
      putString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, MediaStore.getVersion(ctx))
 | 
			
		||||
      putString(SHARED_PREF_MEDIA_STORE_GEN_KEY, Json.encodeToString(genMap))
 | 
			
		||||
      apply()
 | 
			
		||||
    }
 | 
			
		||||
    callback(Result.success(Unit))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @RequiresApi(Build.VERSION_CODES.Q)
 | 
			
		||||
  fun getAssetIdsForAlbum(albumId: String, callback: (Result<List<String>>) -> Unit) {
 | 
			
		||||
    try {
 | 
			
		||||
  fun getAssetIdsForAlbum(albumId: String): List<String> {
 | 
			
		||||
    val uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
 | 
			
		||||
    val projection = arrayOf(MediaStore.Files.FileColumns._ID)
 | 
			
		||||
    val selection =
 | 
			
		||||
@ -64,29 +56,22 @@ class MediaManager(context: Context) {
 | 
			
		||||
      MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString()
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
      val ids =
 | 
			
		||||
        ctx.contentResolver.query(uri, projection, selection, selectionArgs, null)?.use { cursor ->
 | 
			
		||||
    return ctx.contentResolver.query(uri, projection, selection, selectionArgs, null)
 | 
			
		||||
      ?.use { cursor ->
 | 
			
		||||
        val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)
 | 
			
		||||
        generateSequence {
 | 
			
		||||
          if (cursor.moveToNext()) cursor.getLong(idColumn).toString() else null
 | 
			
		||||
        }.toList()
 | 
			
		||||
      } ?: emptyList()
 | 
			
		||||
 | 
			
		||||
      callback(Result.success(ids))
 | 
			
		||||
    } catch (e: Exception) {
 | 
			
		||||
      callback(Result.failure(e))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @RequiresApi(Build.VERSION_CODES.R)
 | 
			
		||||
  @RequiresExtension(extension = Build.VERSION_CODES.R, version = 1)
 | 
			
		||||
  fun getMediaChanges(callback: (Result<SyncDelta>) -> Unit) {
 | 
			
		||||
    try {
 | 
			
		||||
  fun getMediaChanges(): SyncDelta {
 | 
			
		||||
    val genMap = getSavedGenerationMap(ctx)
 | 
			
		||||
    val currentVolumes = MediaStore.getExternalVolumeNames(ctx)
 | 
			
		||||
    val changed = mutableListOf<Asset>()
 | 
			
		||||
    val deleted = mutableListOf<String>()
 | 
			
		||||
      val formatter = DateTimeFormatter.ISO_DATE_TIME.withZone(ZoneOffset.UTC)
 | 
			
		||||
 | 
			
		||||
    var hasChanges = genMap.keys != currentVolumes
 | 
			
		||||
    for (volume in currentVolumes) {
 | 
			
		||||
@ -149,13 +134,10 @@ class MediaManager(context: Context) {
 | 
			
		||||
          val mediaType = cursor.getInt(mediaTypeColumn)
 | 
			
		||||
          val name = cursor.getString(nameColumn)
 | 
			
		||||
          // Date taken is milliseconds since epoch, Date added is seconds since epoch
 | 
			
		||||
            val takenAt = cursor.getLong(dateTakenColumn).takeIf { it > 0 } ?: (cursor.getLong(
 | 
			
		||||
              dateAddedColumn
 | 
			
		||||
            ) * 1000)
 | 
			
		||||
            val createdAt = formatter.format(Instant.ofEpochMilli(takenAt))
 | 
			
		||||
          val createdAt = (cursor.getLong(dateTakenColumn).takeIf { it > 0 }?.div(1000))
 | 
			
		||||
            ?: cursor.getLong(dateAddedColumn)
 | 
			
		||||
          // Date modified is seconds since epoch
 | 
			
		||||
            val modifiedAt =
 | 
			
		||||
              formatter.format(Instant.ofEpochMilli(cursor.getLong(dateModifiedColumn) * 1000))
 | 
			
		||||
          val modifiedAt = cursor.getLong(dateModifiedColumn) * 1000
 | 
			
		||||
          // Duration is milliseconds
 | 
			
		||||
          val duration =
 | 
			
		||||
            if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0 else cursor.getLong(
 | 
			
		||||
@ -179,9 +161,6 @@ class MediaManager(context: Context) {
 | 
			
		||||
    }
 | 
			
		||||
    // Unmounted volumes are handled in dart when the album is removed
 | 
			
		||||
 | 
			
		||||
      callback(Result.success(SyncDelta(hasChanges, changed, deleted)))
 | 
			
		||||
    } catch (e: Exception) {
 | 
			
		||||
      callback(Result.failure(e))
 | 
			
		||||
    }
 | 
			
		||||
    return SyncDelta(hasChanges, changed, deleted)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -82,8 +82,8 @@ data class Asset (
 | 
			
		||||
  val id: String,
 | 
			
		||||
  val name: String,
 | 
			
		||||
  val type: Long,
 | 
			
		||||
  val createdAt: String? = null,
 | 
			
		||||
  val updatedAt: String? = null,
 | 
			
		||||
  val createdAt: Long? = null,
 | 
			
		||||
  val updatedAt: Long? = null,
 | 
			
		||||
  val durationInSeconds: Long,
 | 
			
		||||
  val albumIds: List<String>
 | 
			
		||||
)
 | 
			
		||||
@ -93,8 +93,8 @@ data class Asset (
 | 
			
		||||
      val id = pigeonVar_list[0] as String
 | 
			
		||||
      val name = pigeonVar_list[1] as String
 | 
			
		||||
      val type = pigeonVar_list[2] as Long
 | 
			
		||||
      val createdAt = pigeonVar_list[3] as String?
 | 
			
		||||
      val updatedAt = pigeonVar_list[4] as String?
 | 
			
		||||
      val createdAt = pigeonVar_list[3] as Long?
 | 
			
		||||
      val updatedAt = pigeonVar_list[4] as Long?
 | 
			
		||||
      val durationInSeconds = pigeonVar_list[5] as Long
 | 
			
		||||
      val albumIds = pigeonVar_list[6] as List<String>
 | 
			
		||||
      return Asset(id, name, type, createdAt, updatedAt, durationInSeconds, albumIds)
 | 
			
		||||
@ -187,13 +187,12 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
 | 
			
		||||
interface ImHostService {
 | 
			
		||||
  fun shouldFullSync(callback: (Result<Boolean>) -> Unit)
 | 
			
		||||
  fun getMediaChanges(callback: (Result<SyncDelta>) -> Unit)
 | 
			
		||||
  fun checkpointSync(callback: (Result<Unit>) -> Unit)
 | 
			
		||||
  fun getAssetIdsForAlbum(albumId: String, callback: (Result<List<String>>) -> Unit)
 | 
			
		||||
  fun shouldFullSync(): Boolean
 | 
			
		||||
  fun getMediaChanges(): SyncDelta
 | 
			
		||||
  fun checkpointSync()
 | 
			
		||||
  fun getAssetIdsForAlbum(albumId: String): List<String>
 | 
			
		||||
 | 
			
		||||
  companion object {
 | 
			
		||||
    /** The codec used by ImHostService. */
 | 
			
		||||
@ -209,15 +208,12 @@ interface ImHostService {
 | 
			
		||||
        val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostService.shouldFullSync$separatedMessageChannelSuffix", codec)
 | 
			
		||||
        if (api != null) {
 | 
			
		||||
          channel.setMessageHandler { _, reply ->
 | 
			
		||||
            api.shouldFullSync{ result: Result<Boolean> ->
 | 
			
		||||
              val error = result.exceptionOrNull()
 | 
			
		||||
              if (error != null) {
 | 
			
		||||
                reply.reply(MessagesPigeonUtils.wrapError(error))
 | 
			
		||||
              } else {
 | 
			
		||||
                val data = result.getOrNull()
 | 
			
		||||
                reply.reply(MessagesPigeonUtils.wrapResult(data))
 | 
			
		||||
              }
 | 
			
		||||
            val wrapped: List<Any?> = try {
 | 
			
		||||
              listOf(api.shouldFullSync())
 | 
			
		||||
            } catch (exception: Throwable) {
 | 
			
		||||
              MessagesPigeonUtils.wrapError(exception)
 | 
			
		||||
            }
 | 
			
		||||
            reply.reply(wrapped)
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          channel.setMessageHandler(null)
 | 
			
		||||
@ -227,15 +223,12 @@ interface ImHostService {
 | 
			
		||||
        val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostService.getMediaChanges$separatedMessageChannelSuffix", codec, taskQueue)
 | 
			
		||||
        if (api != null) {
 | 
			
		||||
          channel.setMessageHandler { _, reply ->
 | 
			
		||||
            api.getMediaChanges{ result: Result<SyncDelta> ->
 | 
			
		||||
              val error = result.exceptionOrNull()
 | 
			
		||||
              if (error != null) {
 | 
			
		||||
                reply.reply(MessagesPigeonUtils.wrapError(error))
 | 
			
		||||
              } else {
 | 
			
		||||
                val data = result.getOrNull()
 | 
			
		||||
                reply.reply(MessagesPigeonUtils.wrapResult(data))
 | 
			
		||||
              }
 | 
			
		||||
            val wrapped: List<Any?> = try {
 | 
			
		||||
              listOf(api.getMediaChanges())
 | 
			
		||||
            } catch (exception: Throwable) {
 | 
			
		||||
              MessagesPigeonUtils.wrapError(exception)
 | 
			
		||||
            }
 | 
			
		||||
            reply.reply(wrapped)
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          channel.setMessageHandler(null)
 | 
			
		||||
@ -245,14 +238,13 @@ interface ImHostService {
 | 
			
		||||
        val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostService.checkpointSync$separatedMessageChannelSuffix", codec)
 | 
			
		||||
        if (api != null) {
 | 
			
		||||
          channel.setMessageHandler { _, reply ->
 | 
			
		||||
            api.checkpointSync{ result: Result<Unit> ->
 | 
			
		||||
              val error = result.exceptionOrNull()
 | 
			
		||||
              if (error != null) {
 | 
			
		||||
                reply.reply(MessagesPigeonUtils.wrapError(error))
 | 
			
		||||
              } else {
 | 
			
		||||
                reply.reply(MessagesPigeonUtils.wrapResult(null))
 | 
			
		||||
              }
 | 
			
		||||
            val wrapped: List<Any?> = try {
 | 
			
		||||
              api.checkpointSync()
 | 
			
		||||
              listOf(null)
 | 
			
		||||
            } catch (exception: Throwable) {
 | 
			
		||||
              MessagesPigeonUtils.wrapError(exception)
 | 
			
		||||
            }
 | 
			
		||||
            reply.reply(wrapped)
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          channel.setMessageHandler(null)
 | 
			
		||||
@ -264,15 +256,12 @@ interface ImHostService {
 | 
			
		||||
          channel.setMessageHandler { message, reply ->
 | 
			
		||||
            val args = message as List<Any?>
 | 
			
		||||
            val albumIdArg = args[0] as String
 | 
			
		||||
            api.getAssetIdsForAlbum(albumIdArg) { result: Result<List<String>> ->
 | 
			
		||||
              val error = result.exceptionOrNull()
 | 
			
		||||
              if (error != null) {
 | 
			
		||||
                reply.reply(MessagesPigeonUtils.wrapError(error))
 | 
			
		||||
              } else {
 | 
			
		||||
                val data = result.getOrNull()
 | 
			
		||||
                reply.reply(MessagesPigeonUtils.wrapResult(data))
 | 
			
		||||
              }
 | 
			
		||||
            val wrapped: List<Any?> = try {
 | 
			
		||||
              listOf(api.getAssetIdsForAlbum(albumIdArg))
 | 
			
		||||
            } catch (exception: Throwable) {
 | 
			
		||||
              MessagesPigeonUtils.wrapError(exception)
 | 
			
		||||
            }
 | 
			
		||||
            reply.reply(wrapped)
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          channel.setMessageHandler(null)
 | 
			
		||||
 | 
			
		||||
@ -20,38 +20,27 @@ class MessagesImpl(context: Context) : ImHostService {
 | 
			
		||||
      IllegalStateException("Method not supported on this Android version.")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun shouldFullSync(callback: (Result<Boolean>) -> Unit) {
 | 
			
		||||
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
 | 
			
		||||
      mediaManager.shouldFullSync(callback)
 | 
			
		||||
    } else {
 | 
			
		||||
      callback(Result.success(true))
 | 
			
		||||
  override fun shouldFullSync(): Boolean =
 | 
			
		||||
    Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || mediaManager.shouldFullSync()
 | 
			
		||||
 | 
			
		||||
  override fun getMediaChanges(): SyncDelta {
 | 
			
		||||
    if (!isMediaChangesSupported()) {
 | 
			
		||||
      throw unsupportedFeatureException()
 | 
			
		||||
    }
 | 
			
		||||
    return mediaManager.getMediaChanges()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun getMediaChanges(callback: (Result<SyncDelta>) -> Unit) {
 | 
			
		||||
    if (isMediaChangesSupported()) {
 | 
			
		||||
      mediaManager.getMediaChanges(callback)
 | 
			
		||||
    } else {
 | 
			
		||||
      callback(Result.failure(unsupportedFeatureException()))
 | 
			
		||||
  override fun checkpointSync() {
 | 
			
		||||
    if (!isMediaChangesSupported()) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    mediaManager.checkpointSync()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun checkpointSync(callback: (Result<Unit>) -> Unit) {
 | 
			
		||||
    if (isMediaChangesSupported()) {
 | 
			
		||||
      mediaManager.checkpointSync(callback)
 | 
			
		||||
    } else {
 | 
			
		||||
      callback(Result.success(Unit))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun getAssetIdsForAlbum(
 | 
			
		||||
    albumId: String,
 | 
			
		||||
    callback: (Result<List<String>>) -> Unit
 | 
			
		||||
  ) {
 | 
			
		||||
    if (isMediaChangesSupported()) {
 | 
			
		||||
      mediaManager.getAssetIdsForAlbum(albumId, callback)
 | 
			
		||||
    } else {
 | 
			
		||||
      callback(Result.failure(unsupportedFeatureException()))
 | 
			
		||||
    }
 | 
			
		||||
  override fun getAssetIdsForAlbum(albumId: String): List<String> {
 | 
			
		||||
    if (!isMediaChangesSupported()) {
 | 
			
		||||
      throw unsupportedFeatureException()
 | 
			
		||||
    }
 | 
			
		||||
    return mediaManager.getAssetIdsForAlbum(albumId)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import Photos
 | 
			
		||||
 | 
			
		||||
class WrapperAsset: Hashable, Equatable {
 | 
			
		||||
  var asset: Asset
 | 
			
		||||
struct AssetWrapper: Hashable, Equatable {
 | 
			
		||||
  let asset: Asset
 | 
			
		||||
  
 | 
			
		||||
  init(with asset: Asset) {
 | 
			
		||||
    self.asset = asset
 | 
			
		||||
@ -11,22 +11,22 @@ class WrapperAsset: Hashable, Equatable {
 | 
			
		||||
    hasher.combine(self.asset.id)
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  static func == (lhs: WrapperAsset, rhs: WrapperAsset) -> Bool {
 | 
			
		||||
  static func == (lhs: AssetWrapper, rhs: AssetWrapper) -> Bool {
 | 
			
		||||
    return lhs.asset.id == rhs.asset.id
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class MediaManager {
 | 
			
		||||
  private let _defaults: UserDefaults
 | 
			
		||||
  private let _changeTokenKey = "immich:changeToken"
 | 
			
		||||
  private let defaults: UserDefaults
 | 
			
		||||
  private let changeTokenKey = "immich:changeToken"
 | 
			
		||||
  
 | 
			
		||||
  init(with defaults: UserDefaults = .standard) {
 | 
			
		||||
    self._defaults = defaults
 | 
			
		||||
    self.defaults = defaults
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @available(iOS 16, *)
 | 
			
		||||
  func _getChangeToken() -> PHPersistentChangeToken? {
 | 
			
		||||
    guard let encodedToken = _defaults.data(forKey: _changeTokenKey) else {
 | 
			
		||||
  private func getChangeToken() -> PHPersistentChangeToken? {
 | 
			
		||||
    guard let encodedToken = defaults.data(forKey: changeTokenKey) else {
 | 
			
		||||
        print("MediaManager::_getChangeToken: Change token not available in UserDefaults")
 | 
			
		||||
        return nil
 | 
			
		||||
    }
 | 
			
		||||
@ -40,10 +40,10 @@ class MediaManager {
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  @available(iOS 16, *)
 | 
			
		||||
  func _saveChangeToken(token: PHPersistentChangeToken) -> Void {
 | 
			
		||||
  private func saveChangeToken(token: PHPersistentChangeToken) -> Void {
 | 
			
		||||
    do {
 | 
			
		||||
        let encodedToken = try NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true)
 | 
			
		||||
        _defaults.set(encodedToken, forKey: _changeTokenKey)
 | 
			
		||||
        defaults.set(encodedToken, forKey: changeTokenKey)
 | 
			
		||||
        print("MediaManager::_setChangeToken: Change token saved to UserDefaults")
 | 
			
		||||
    } catch {
 | 
			
		||||
        print("MediaManager::_setChangeToken: Failed to persist the token to UserDefaults: \(error)")
 | 
			
		||||
@ -51,109 +51,96 @@ class MediaManager {
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  @available(iOS 16, *)
 | 
			
		||||
  func checkpointSync(completion: @escaping (Result<Void, any Error>) -> Void) {
 | 
			
		||||
    _saveChangeToken(token: PHPhotoLibrary.shared().currentChangeToken)
 | 
			
		||||
    completion(.success(()))
 | 
			
		||||
  func checkpointSync() {
 | 
			
		||||
    saveChangeToken(token: PHPhotoLibrary.shared().currentChangeToken)
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  @available(iOS 16, *)
 | 
			
		||||
  func shouldFullSync(completion: @escaping (Result<Bool, Error>) -> Void) {
 | 
			
		||||
  func shouldFullSync() -> Bool {
 | 
			
		||||
    guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else {
 | 
			
		||||
      // When we do not have access to photo library, return true to fallback to old sync
 | 
			
		||||
      completion(.success(true))
 | 
			
		||||
      return
 | 
			
		||||
      return true
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    guard let storedToken = _getChangeToken() else {
 | 
			
		||||
    guard let storedToken = getChangeToken() else {
 | 
			
		||||
       // No token exists, perform the initial full sync
 | 
			
		||||
       print("MediaManager::shouldUseOldSync: No token found. Full sync required")
 | 
			
		||||
       completion(.success(true))
 | 
			
		||||
       return
 | 
			
		||||
       return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    do {
 | 
			
		||||
        _ = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken)
 | 
			
		||||
      completion(.success(false))
 | 
			
		||||
      return false
 | 
			
		||||
    } catch {
 | 
			
		||||
      // fallback to using old sync when we cannot detect changes using the available token
 | 
			
		||||
      print("MediaManager::shouldUseOldSync: fetchPersistentChanges failed with error (\(error))")
 | 
			
		||||
      completion(.success(true))
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    return true
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @available(iOS 16, *)
 | 
			
		||||
  func getMediaChanges(completion: @escaping (Result<SyncDelta, Error>) -> Void) {
 | 
			
		||||
  func getMediaChanges() throws -> SyncDelta {
 | 
			
		||||
    guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else {
 | 
			
		||||
      completion(.failure(PigeonError(code: "NO_AUTH", message: "No photo library access", details: nil)))
 | 
			
		||||
      return
 | 
			
		||||
      throw PigeonError(code: "NO_AUTH", message: "No photo library access", details: nil)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    guard let storedToken = _getChangeToken() else {
 | 
			
		||||
    guard let storedToken = getChangeToken() else {
 | 
			
		||||
       // No token exists, definitely need a full sync
 | 
			
		||||
       print("MediaManager::getMediaChanges: No token found")
 | 
			
		||||
       completion(.failure(PigeonError(code: "NO_TOKEN", message: "No stored change token", details: nil)))
 | 
			
		||||
       return
 | 
			
		||||
       throw PigeonError(code: "NO_TOKEN", message: "No stored change token", details: nil)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    let currentToken = PHPhotoLibrary.shared().currentChangeToken
 | 
			
		||||
    if storedToken == currentToken {
 | 
			
		||||
      completion(.success(SyncDelta(hasChanges: false, updates: [], deletes: [])))
 | 
			
		||||
      return
 | 
			
		||||
      return SyncDelta(hasChanges: false, updates: [], deletes: [])
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    do {
 | 
			
		||||
      let result = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken)
 | 
			
		||||
      let dateFormatter = ISO8601DateFormatter()
 | 
			
		||||
      dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
 | 
			
		||||
      let changes = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken)
 | 
			
		||||
      
 | 
			
		||||
      var updatedArr: Set<WrapperAsset> = []
 | 
			
		||||
      var deletedArr: Set<String> = []
 | 
			
		||||
      var updatedAssets: Set<AssetWrapper> = []
 | 
			
		||||
      var deletedAssets: Set<String> = []
 | 
			
		||||
      
 | 
			
		||||
      for changes in result {
 | 
			
		||||
        let details = try changes.changeDetails(for: PHObjectType.asset)
 | 
			
		||||
      for change in changes {
 | 
			
		||||
        guard let details = try? change.changeDetails(for: PHObjectType.asset) else { continue }
 | 
			
		||||
        
 | 
			
		||||
        let updated = details.updatedLocalIdentifiers.union(details.insertedLocalIdentifiers)
 | 
			
		||||
        let deleted = details.deletedLocalIdentifiers
 | 
			
		||||
        if (updated.isEmpty) { continue }
 | 
			
		||||
        
 | 
			
		||||
        let options = PHFetchOptions()
 | 
			
		||||
        options.includeHiddenAssets = false
 | 
			
		||||
        let result = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: nil)
 | 
			
		||||
        for i in 0..<result.count {
 | 
			
		||||
          let asset = result.object(at: i)
 | 
			
		||||
          
 | 
			
		||||
        let updatedAssets = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: options)
 | 
			
		||||
 | 
			
		||||
        updatedAssets.enumerateObjects { (asset, _, _) in
 | 
			
		||||
          // Asset wrapper only uses the id for comparison. Multiple change can contain the same asset, skip duplicate changes
 | 
			
		||||
          let predicate = Asset(id: asset.localIdentifier, name: "", type: 0, createdAt: nil, updatedAt: nil, durationInSeconds: 0, albumIds: [])
 | 
			
		||||
          if (updatedAssets.contains(AssetWrapper(with: predicate))) {
 | 
			
		||||
            continue
 | 
			
		||||
          }
 | 
			
		||||
          
 | 
			
		||||
          let id = asset.localIdentifier
 | 
			
		||||
          let name = PHAssetResource.assetResources(for: asset).first?.originalFilename ?? asset.title()
 | 
			
		||||
          let type: Int64 = Int64(asset.mediaType.rawValue)
 | 
			
		||||
          let createdAt = asset.creationDate.map { dateFormatter.string(from: $0) }
 | 
			
		||||
          let updatedAt = asset.modificationDate.map { dateFormatter.string(from: $0) }
 | 
			
		||||
          let createdAt = asset.creationDate?.timeIntervalSince1970
 | 
			
		||||
          let updatedAt = asset.modificationDate?.timeIntervalSince1970
 | 
			
		||||
          let durationInSeconds: Int64 = Int64(asset.duration)
 | 
			
		||||
          
 | 
			
		||||
          let domainAsset = WrapperAsset(with: Asset(
 | 
			
		||||
          let domainAsset = AssetWrapper(with: Asset(
 | 
			
		||||
            id: id,
 | 
			
		||||
            name: name,
 | 
			
		||||
            type: type,
 | 
			
		||||
              createdAt: createdAt,
 | 
			
		||||
              updatedAt: updatedAt,
 | 
			
		||||
            createdAt:  createdAt.map { Int64($0) },
 | 
			
		||||
            updatedAt: updatedAt.map { Int64($0) },
 | 
			
		||||
            durationInSeconds: durationInSeconds,
 | 
			
		||||
            albumIds: self._getAlbumIds(forAsset: asset)
 | 
			
		||||
          ))
 | 
			
		||||
          
 | 
			
		||||
          updatedArr.insert(domainAsset)
 | 
			
		||||
          updatedAssets.insert(domainAsset)
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        deletedArr.formUnion(deleted)
 | 
			
		||||
        deletedAssets.formUnion(details.deletedLocalIdentifiers)
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      let delta = SyncDelta(hasChanges: true, updates: Array(updatedArr.map { $0.asset }), deletes: Array(deletedArr))
 | 
			
		||||
    
 | 
			
		||||
      completion(.success(delta))
 | 
			
		||||
      return
 | 
			
		||||
    } catch {
 | 
			
		||||
      print("MediaManager::getMediaChanges: Error fetching persistent changes: \(error)")
 | 
			
		||||
      completion(.failure(PigeonError(code: "3", message: error.localizedDescription, details: nil)))
 | 
			
		||||
      return
 | 
			
		||||
      return SyncDelta(hasChanges: true, updates: Array(updatedAssets.map { $0.asset }), deletes: Array(deletedAssets))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
@ -133,8 +133,8 @@ struct Asset: Hashable {
 | 
			
		||||
  var id: String
 | 
			
		||||
  var name: String
 | 
			
		||||
  var type: Int64
 | 
			
		||||
  var createdAt: String? = nil
 | 
			
		||||
  var updatedAt: String? = nil
 | 
			
		||||
  var createdAt: Int64? = nil
 | 
			
		||||
  var updatedAt: Int64? = nil
 | 
			
		||||
  var durationInSeconds: Int64
 | 
			
		||||
  var albumIds: [String]
 | 
			
		||||
 | 
			
		||||
@ -144,8 +144,8 @@ struct Asset: Hashable {
 | 
			
		||||
    let id = pigeonVar_list[0] as! String
 | 
			
		||||
    let name = pigeonVar_list[1] as! String
 | 
			
		||||
    let type = pigeonVar_list[2] as! Int64
 | 
			
		||||
    let createdAt: String? = nilOrValue(pigeonVar_list[3])
 | 
			
		||||
    let updatedAt: String? = nilOrValue(pigeonVar_list[4])
 | 
			
		||||
    let createdAt: Int64? = nilOrValue(pigeonVar_list[3])
 | 
			
		||||
    let updatedAt: Int64? = nilOrValue(pigeonVar_list[4])
 | 
			
		||||
    let durationInSeconds = pigeonVar_list[5] as! Int64
 | 
			
		||||
    let albumIds = pigeonVar_list[6] as! [String]
 | 
			
		||||
 | 
			
		||||
@ -251,13 +251,12 @@ class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
 | 
			
		||||
  static let shared = MessagesPigeonCodec(readerWriter: MessagesPigeonCodecReaderWriter())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
 | 
			
		||||
protocol ImHostService {
 | 
			
		||||
  func shouldFullSync(completion: @escaping (Result<Bool, Error>) -> Void)
 | 
			
		||||
  func getMediaChanges(completion: @escaping (Result<SyncDelta, Error>) -> Void)
 | 
			
		||||
  func checkpointSync(completion: @escaping (Result<Void, Error>) -> Void)
 | 
			
		||||
  func getAssetIdsForAlbum(albumId: String, completion: @escaping (Result<[String], Error>) -> Void)
 | 
			
		||||
  func shouldFullSync() throws -> Bool
 | 
			
		||||
  func getMediaChanges() throws -> SyncDelta
 | 
			
		||||
  func checkpointSync() throws
 | 
			
		||||
  func getAssetIdsForAlbum(albumId: String) throws -> [String]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
 | 
			
		||||
@ -274,15 +273,13 @@ class ImHostServiceSetup {
 | 
			
		||||
    let shouldFullSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostService.shouldFullSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
 | 
			
		||||
    if let api = api {
 | 
			
		||||
      shouldFullSyncChannel.setMessageHandler { _, reply in
 | 
			
		||||
        api.shouldFullSync { result in
 | 
			
		||||
          switch result {
 | 
			
		||||
          case .success(let res):
 | 
			
		||||
            reply(wrapResult(res))
 | 
			
		||||
          case .failure(let error):
 | 
			
		||||
        do {
 | 
			
		||||
          let result = try api.shouldFullSync()
 | 
			
		||||
          reply(wrapResult(result))
 | 
			
		||||
        } catch {
 | 
			
		||||
          reply(wrapError(error))
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      shouldFullSyncChannel.setMessageHandler(nil)
 | 
			
		||||
    }
 | 
			
		||||
@ -291,30 +288,26 @@ class ImHostServiceSetup {
 | 
			
		||||
      : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostService.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
 | 
			
		||||
    if let api = api {
 | 
			
		||||
      getMediaChangesChannel.setMessageHandler { _, reply in
 | 
			
		||||
        api.getMediaChanges { result in
 | 
			
		||||
          switch result {
 | 
			
		||||
          case .success(let res):
 | 
			
		||||
            reply(wrapResult(res))
 | 
			
		||||
          case .failure(let error):
 | 
			
		||||
        do {
 | 
			
		||||
          let result = try api.getMediaChanges()
 | 
			
		||||
          reply(wrapResult(result))
 | 
			
		||||
        } catch {
 | 
			
		||||
          reply(wrapError(error))
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      getMediaChangesChannel.setMessageHandler(nil)
 | 
			
		||||
    }
 | 
			
		||||
    let checkpointSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostService.checkpointSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
 | 
			
		||||
    if let api = api {
 | 
			
		||||
      checkpointSyncChannel.setMessageHandler { _, reply in
 | 
			
		||||
        api.checkpointSync { result in
 | 
			
		||||
          switch result {
 | 
			
		||||
          case .success:
 | 
			
		||||
        do {
 | 
			
		||||
          try api.checkpointSync()
 | 
			
		||||
          reply(wrapResult(nil))
 | 
			
		||||
          case .failure(let error):
 | 
			
		||||
        } catch {
 | 
			
		||||
          reply(wrapError(error))
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      checkpointSyncChannel.setMessageHandler(nil)
 | 
			
		||||
    }
 | 
			
		||||
@ -325,15 +318,13 @@ class ImHostServiceSetup {
 | 
			
		||||
      getAssetIdsForAlbumChannel.setMessageHandler { message, reply in
 | 
			
		||||
        let args = message as! [Any?]
 | 
			
		||||
        let albumIdArg = args[0] as! String
 | 
			
		||||
        api.getAssetIdsForAlbum(albumId: albumIdArg) { result in
 | 
			
		||||
          switch result {
 | 
			
		||||
          case .success(let res):
 | 
			
		||||
            reply(wrapResult(res))
 | 
			
		||||
          case .failure(let error):
 | 
			
		||||
        do {
 | 
			
		||||
          let result = try api.getAssetIdsForAlbum(albumId: albumIdArg)
 | 
			
		||||
          reply(wrapResult(result))
 | 
			
		||||
        } catch {
 | 
			
		||||
          reply(wrapError(error))
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      getAssetIdsForAlbumChannel.setMessageHandler(nil)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,39 +1,36 @@
 | 
			
		||||
import Photos
 | 
			
		||||
 | 
			
		||||
class ImHostServiceImpl: ImHostService {
 | 
			
		||||
  let _mediaManager: MediaManager
 | 
			
		||||
  private let mediaManager: MediaManager
 | 
			
		||||
  
 | 
			
		||||
  init() {
 | 
			
		||||
    self._mediaManager = MediaManager()
 | 
			
		||||
    self.mediaManager = MediaManager()
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  func shouldFullSync(completion: @escaping (Result<Bool, Error>) -> Void) {
 | 
			
		||||
    if #available(iOS 16, *) {
 | 
			
		||||
      _mediaManager.shouldFullSync(completion: completion)
 | 
			
		||||
  func shouldFullSync() throws -> Bool {
 | 
			
		||||
    return if #available(iOS 16, *) {
 | 
			
		||||
      mediaManager.shouldFullSync()
 | 
			
		||||
    } else {
 | 
			
		||||
      // Always fall back to full sync on older iOS versions
 | 
			
		||||
      completion(.success(true))
 | 
			
		||||
      true
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  func getMediaChanges(completion: @escaping (Result<SyncDelta, Error>) -> Void) {
 | 
			
		||||
  func getMediaChanges() throws -> SyncDelta {
 | 
			
		||||
    guard #available(iOS 16, *) else {
 | 
			
		||||
      completion(.failure(PigeonError(code: "UNSUPPORTED_OS", message: "This feature requires iOS 16 or later.", details: nil)))
 | 
			
		||||
      return
 | 
			
		||||
      throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature requires iOS 16 or later.", details: nil)
 | 
			
		||||
    }
 | 
			
		||||
    _mediaManager.getMediaChanges(completion: completion)
 | 
			
		||||
    return try mediaManager.getMediaChanges()
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  func checkpointSync(completion: @escaping (Result<Void, any Error>) -> Void) {
 | 
			
		||||
  func checkpointSync() throws {
 | 
			
		||||
    if #available(iOS 16, *) {
 | 
			
		||||
      _mediaManager.checkpointSync(completion: completion)
 | 
			
		||||
    } else {
 | 
			
		||||
      completion(.success(()))
 | 
			
		||||
      mediaManager.checkpointSync()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  func getAssetIdsForAlbum(albumId: String, completion: @escaping (Result<[String], any Error>) -> Void) {
 | 
			
		||||
  func getAssetIdsForAlbum(albumId: String) throws -> [String] {
 | 
			
		||||
    // Android specific, empty list is safe no-op
 | 
			
		||||
    completion(.success([]))
 | 
			
		||||
    return []
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -354,10 +354,12 @@ extension on platform.Asset {
 | 
			
		||||
      id: id,
 | 
			
		||||
      name: name,
 | 
			
		||||
      type: AssetType.values.elementAtOrNull(type) ?? AssetType.other,
 | 
			
		||||
      createdAt:
 | 
			
		||||
          createdAt == null ? DateTime.now() : DateTime.parse(createdAt!),
 | 
			
		||||
      updatedAt:
 | 
			
		||||
          updatedAt == null ? DateTime.now() : DateTime.parse(updatedAt!),
 | 
			
		||||
      createdAt: createdAt == null
 | 
			
		||||
          ? DateTime.now()
 | 
			
		||||
          : DateTime.fromMillisecondsSinceEpoch(createdAt! * 1000),
 | 
			
		||||
      updatedAt: updatedAt == null
 | 
			
		||||
          ? DateTime.now()
 | 
			
		||||
          : DateTime.fromMillisecondsSinceEpoch(updatedAt! * 1000),
 | 
			
		||||
      durationInSeconds: durationInSeconds,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -16,8 +16,9 @@ class Asset {
 | 
			
		||||
  final String id;
 | 
			
		||||
  final String name;
 | 
			
		||||
  final int type; // follows AssetType enum from base_asset.model.dart
 | 
			
		||||
  final String? createdAt;
 | 
			
		||||
  final String? updatedAt;
 | 
			
		||||
  // Seconds since epoch
 | 
			
		||||
  final int? createdAt;
 | 
			
		||||
  final int? updatedAt;
 | 
			
		||||
  final int durationInSeconds;
 | 
			
		||||
  final List<String> albumIds;
 | 
			
		||||
 | 
			
		||||
@ -25,10 +26,10 @@ class Asset {
 | 
			
		||||
    required this.id,
 | 
			
		||||
    required this.name,
 | 
			
		||||
    required this.type,
 | 
			
		||||
    required this.createdAt,
 | 
			
		||||
    required this.updatedAt,
 | 
			
		||||
    required this.durationInSeconds,
 | 
			
		||||
    required this.albumIds,
 | 
			
		||||
    this.createdAt,
 | 
			
		||||
    this.updatedAt,
 | 
			
		||||
    this.durationInSeconds = 0,
 | 
			
		||||
    this.albumIds = const [],
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -45,17 +46,13 @@ class SyncDelta {
 | 
			
		||||
 | 
			
		||||
@HostApi()
 | 
			
		||||
abstract class ImHostService {
 | 
			
		||||
  @async
 | 
			
		||||
  bool shouldFullSync();
 | 
			
		||||
 | 
			
		||||
  @async
 | 
			
		||||
  @TaskQueue(type: TaskQueueType.serialBackgroundThread)
 | 
			
		||||
  SyncDelta getMediaChanges();
 | 
			
		||||
 | 
			
		||||
  @async
 | 
			
		||||
  void checkpointSync();
 | 
			
		||||
 | 
			
		||||
  @async
 | 
			
		||||
  @TaskQueue(type: TaskQueueType.serialBackgroundThread)
 | 
			
		||||
  List<String> getAssetIdsForAlbum(String albumId);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										59
									
								
								mobile/lib/platform/messages.g.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										59
									
								
								mobile/lib/platform/messages.g.dart
									
									
									
										generated
									
									
									
								
							@ -14,7 +14,6 @@ PlatformException _createConnectionError(String channelName) {
 | 
			
		||||
    message: 'Unable to establish connection on channel: "$channelName".',
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool _deepEquals(Object? a, Object? b) {
 | 
			
		||||
  if (a is List && b is List) {
 | 
			
		||||
    return a.length == b.length &&
 | 
			
		||||
@ -22,14 +21,14 @@ bool _deepEquals(Object? a, Object? b) {
 | 
			
		||||
        .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
 | 
			
		||||
  }
 | 
			
		||||
  if (a is Map && b is Map) {
 | 
			
		||||
    return a.length == b.length &&
 | 
			
		||||
        a.entries.every((MapEntry<Object?, Object?> entry) =>
 | 
			
		||||
    return a.length == b.length && a.entries.every((MapEntry<Object?, Object?> entry) =>
 | 
			
		||||
        (b as Map<Object?, Object?>).containsKey(entry.key) &&
 | 
			
		||||
        _deepEquals(entry.value, b[entry.key]));
 | 
			
		||||
  }
 | 
			
		||||
  return a == b;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Asset {
 | 
			
		||||
  Asset({
 | 
			
		||||
    required this.id,
 | 
			
		||||
@ -47,9 +46,9 @@ class Asset {
 | 
			
		||||
 | 
			
		||||
  int type;
 | 
			
		||||
 | 
			
		||||
  String? createdAt;
 | 
			
		||||
  int? createdAt;
 | 
			
		||||
 | 
			
		||||
  String? updatedAt;
 | 
			
		||||
  int? updatedAt;
 | 
			
		||||
 | 
			
		||||
  int durationInSeconds;
 | 
			
		||||
 | 
			
		||||
@ -68,8 +67,7 @@ class Asset {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Object encode() {
 | 
			
		||||
    return _toList();
 | 
			
		||||
  }
 | 
			
		||||
    return _toList();  }
 | 
			
		||||
 | 
			
		||||
  static Asset decode(Object result) {
 | 
			
		||||
    result as List<Object?>;
 | 
			
		||||
@ -77,8 +75,8 @@ class Asset {
 | 
			
		||||
      id: result[0]! as String,
 | 
			
		||||
      name: result[1]! as String,
 | 
			
		||||
      type: result[2]! as int,
 | 
			
		||||
      createdAt: result[3] as String?,
 | 
			
		||||
      updatedAt: result[4] as String?,
 | 
			
		||||
      createdAt: result[3] as int?,
 | 
			
		||||
      updatedAt: result[4] as int?,
 | 
			
		||||
      durationInSeconds: result[5]! as int,
 | 
			
		||||
      albumIds: (result[6] as List<Object?>?)!.cast<String>(),
 | 
			
		||||
    );
 | 
			
		||||
@ -98,7 +96,8 @@ class Asset {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  // ignore: avoid_equals_and_hash_code_on_mutable_classes
 | 
			
		||||
  int get hashCode => Object.hashAll(_toList());
 | 
			
		||||
  int get hashCode => Object.hashAll(_toList())
 | 
			
		||||
;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SyncDelta {
 | 
			
		||||
@ -123,8 +122,7 @@ class SyncDelta {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Object encode() {
 | 
			
		||||
    return _toList();
 | 
			
		||||
  }
 | 
			
		||||
    return _toList();  }
 | 
			
		||||
 | 
			
		||||
  static SyncDelta decode(Object result) {
 | 
			
		||||
    result as List<Object?>;
 | 
			
		||||
@ -149,9 +147,11 @@ class SyncDelta {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  // ignore: avoid_equals_and_hash_code_on_mutable_classes
 | 
			
		||||
  int get hashCode => Object.hashAll(_toList());
 | 
			
		||||
  int get hashCode => Object.hashAll(_toList())
 | 
			
		||||
;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class _PigeonCodec extends StandardMessageCodec {
 | 
			
		||||
  const _PigeonCodec();
 | 
			
		||||
  @override
 | 
			
		||||
@ -187,11 +187,9 @@ class ImHostService {
 | 
			
		||||
  /// Constructor for [ImHostService].  The [binaryMessenger] named argument is
 | 
			
		||||
  /// available for dependency injection.  If it is left null, the default
 | 
			
		||||
  /// BinaryMessenger will be used which routes to the host platform.
 | 
			
		||||
  ImHostService(
 | 
			
		||||
      {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
 | 
			
		||||
  ImHostService({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
 | 
			
		||||
      : pigeonVar_binaryMessenger = binaryMessenger,
 | 
			
		||||
        pigeonVar_messageChannelSuffix =
 | 
			
		||||
            messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
 | 
			
		||||
        pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
 | 
			
		||||
  final BinaryMessenger? pigeonVar_binaryMessenger;
 | 
			
		||||
 | 
			
		||||
  static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
 | 
			
		||||
@ -199,10 +197,8 @@ class ImHostService {
 | 
			
		||||
  final String pigeonVar_messageChannelSuffix;
 | 
			
		||||
 | 
			
		||||
  Future<bool> shouldFullSync() async {
 | 
			
		||||
    final String pigeonVar_channelName =
 | 
			
		||||
        'dev.flutter.pigeon.immich_mobile.ImHostService.shouldFullSync$pigeonVar_messageChannelSuffix';
 | 
			
		||||
    final BasicMessageChannel<Object?> pigeonVar_channel =
 | 
			
		||||
        BasicMessageChannel<Object?>(
 | 
			
		||||
    final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.ImHostService.shouldFullSync$pigeonVar_messageChannelSuffix';
 | 
			
		||||
    final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
 | 
			
		||||
      pigeonVar_channelName,
 | 
			
		||||
      pigeonChannelCodec,
 | 
			
		||||
      binaryMessenger: pigeonVar_binaryMessenger,
 | 
			
		||||
@ -229,10 +225,8 @@ class ImHostService {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<SyncDelta> getMediaChanges() async {
 | 
			
		||||
    final String pigeonVar_channelName =
 | 
			
		||||
        'dev.flutter.pigeon.immich_mobile.ImHostService.getMediaChanges$pigeonVar_messageChannelSuffix';
 | 
			
		||||
    final BasicMessageChannel<Object?> pigeonVar_channel =
 | 
			
		||||
        BasicMessageChannel<Object?>(
 | 
			
		||||
    final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.ImHostService.getMediaChanges$pigeonVar_messageChannelSuffix';
 | 
			
		||||
    final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
 | 
			
		||||
      pigeonVar_channelName,
 | 
			
		||||
      pigeonChannelCodec,
 | 
			
		||||
      binaryMessenger: pigeonVar_binaryMessenger,
 | 
			
		||||
@ -259,10 +253,8 @@ class ImHostService {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> checkpointSync() async {
 | 
			
		||||
    final String pigeonVar_channelName =
 | 
			
		||||
        'dev.flutter.pigeon.immich_mobile.ImHostService.checkpointSync$pigeonVar_messageChannelSuffix';
 | 
			
		||||
    final BasicMessageChannel<Object?> pigeonVar_channel =
 | 
			
		||||
        BasicMessageChannel<Object?>(
 | 
			
		||||
    final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.ImHostService.checkpointSync$pigeonVar_messageChannelSuffix';
 | 
			
		||||
    final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
 | 
			
		||||
      pigeonVar_channelName,
 | 
			
		||||
      pigeonChannelCodec,
 | 
			
		||||
      binaryMessenger: pigeonVar_binaryMessenger,
 | 
			
		||||
@ -284,16 +276,13 @@ class ImHostService {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<List<String>> getAssetIdsForAlbum(String albumId) async {
 | 
			
		||||
    final String pigeonVar_channelName =
 | 
			
		||||
        'dev.flutter.pigeon.immich_mobile.ImHostService.getAssetIdsForAlbum$pigeonVar_messageChannelSuffix';
 | 
			
		||||
    final BasicMessageChannel<Object?> pigeonVar_channel =
 | 
			
		||||
        BasicMessageChannel<Object?>(
 | 
			
		||||
    final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.ImHostService.getAssetIdsForAlbum$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]);
 | 
			
		||||
    final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[albumId]);
 | 
			
		||||
    final List<Object?>? pigeonVar_replyList =
 | 
			
		||||
        await pigeonVar_sendFuture as List<Object?>?;
 | 
			
		||||
    if (pigeonVar_replyList == null) {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user