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