You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	feat: delta sync (#18428)
* feat: delta sync * fix: ignore iCloud assets * feat: dev logs * add full sync button * remove photo_manager dep for sync * misc logs and fix * add time taken to DLog * fix: build release iOS * ios sync go brrr * rename local sync service * update isar fork * rename to platform assets / albums * fix ci check --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
		
							
								
								
									
										3
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							| @@ -9,6 +9,9 @@ mobile/lib/**/*.g.dart linguist-generated=true | ||||
| mobile/lib/**/*.drift.dart -diff -merge | ||||
| mobile/lib/**/*.drift.dart linguist-generated=true | ||||
|  | ||||
| mobile/drift_schemas/main/drift_schema_*.json -diff -merge | ||||
| mobile/drift_schemas/main/drift_schema_*.json linguist-generated=true | ||||
|  | ||||
| open-api/typescript-sdk/fetch-client.ts -diff -merge | ||||
| open-api/typescript-sdk/fetch-client.ts linguist-generated=true | ||||
|  | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/build-mobile.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/build-mobile.yml
									
									
									
									
										vendored
									
									
								
							| @@ -93,6 +93,10 @@ jobs: | ||||
|         run: make translation | ||||
|         working-directory: ./mobile | ||||
|  | ||||
|       - name: Generate platform APIs | ||||
|         run: make pigeon | ||||
|         working-directory: ./mobile | ||||
|  | ||||
|       - name: Build Android App Bundle | ||||
|         working-directory: ./mobile | ||||
|         env: | ||||
|   | ||||
							
								
								
									
										6
									
								
								.github/workflows/static_analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/static_analysis.yml
									
									
									
									
										vendored
									
									
								
							| @@ -59,13 +59,17 @@ jobs: | ||||
|         working-directory: ./mobile | ||||
|  | ||||
|       - name: Generate translation file | ||||
|         run: make translation; dart format lib/generated/codegen_loader.g.dart | ||||
|         run: make translation | ||||
|         working-directory: ./mobile | ||||
|  | ||||
|       - name: Run Build Runner | ||||
|         run: make build | ||||
|         working-directory: ./mobile | ||||
|  | ||||
|       - name: Generate platform API | ||||
|         run: make pigeon | ||||
|         working-directory: ./mobile | ||||
|  | ||||
|       - name: Find file changes | ||||
|         uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 | ||||
|         id: verify-changed-files | ||||
|   | ||||
| @@ -55,6 +55,7 @@ custom_lint: | ||||
|       restrict: package:photo_manager | ||||
|       allowed: | ||||
|         # required / wanted | ||||
|         - 'lib/infrastructure/repositories/album_media.repository.dart' | ||||
|         - 'lib/repositories/{album,asset,file}_media.repository.dart' | ||||
|         # acceptable exceptions for the time being | ||||
|         - lib/entities/asset.entity.dart # to provide local AssetEntity for now | ||||
|   | ||||
| @@ -1,103 +1,106 @@ | ||||
| plugins { | ||||
|     id "com.android.application" | ||||
|     id "kotlin-android" | ||||
|     id "dev.flutter.flutter-gradle-plugin" | ||||
|     id 'com.google.devtools.ksp' | ||||
|   id "com.android.application" | ||||
|   id "kotlin-android" | ||||
|   id "dev.flutter.flutter-gradle-plugin" | ||||
|   id 'com.google.devtools.ksp' | ||||
| } | ||||
|  | ||||
| def localProperties = new Properties() | ||||
| def localPropertiesFile = rootProject.file('local.properties') | ||||
| if (localPropertiesFile.exists()) { | ||||
|     localPropertiesFile.withInputStream { localProperties.load(it) } | ||||
|   localPropertiesFile.withInputStream { localProperties.load(it) } | ||||
| } | ||||
|  | ||||
| def flutterVersionCode = localProperties.getProperty('flutter.versionCode') | ||||
| if (flutterVersionCode == null) { | ||||
|     flutterVersionCode = '1' | ||||
|   flutterVersionCode = '1' | ||||
| } | ||||
|  | ||||
| def flutterVersionName = localProperties.getProperty('flutter.versionName') | ||||
| if (flutterVersionName == null) { | ||||
|     flutterVersionName = '1.0' | ||||
|   flutterVersionName = '1.0' | ||||
| } | ||||
|  | ||||
| def keystoreProperties = new Properties() | ||||
| def keystorePropertiesFile = rootProject.file('key.properties') | ||||
| if (keystorePropertiesFile.exists()) { | ||||
|     keystorePropertiesFile.withInputStream { keystoreProperties.load(it) } | ||||
|   keystorePropertiesFile.withInputStream { keystoreProperties.load(it) } | ||||
| } | ||||
|  | ||||
| android { | ||||
|     compileSdkVersion 35 | ||||
|   compileSdkVersion 35 | ||||
|  | ||||
|     compileOptions { | ||||
|         sourceCompatibility JavaVersion.VERSION_17 | ||||
|         targetCompatibility JavaVersion.VERSION_17 | ||||
|         coreLibraryDesugaringEnabled true | ||||
|   compileOptions { | ||||
|     sourceCompatibility JavaVersion.VERSION_17 | ||||
|     targetCompatibility JavaVersion.VERSION_17 | ||||
|     coreLibraryDesugaringEnabled true | ||||
|   } | ||||
|  | ||||
|   kotlinOptions { | ||||
|     jvmTarget = '17' | ||||
|   } | ||||
|  | ||||
|   sourceSets { | ||||
|     main.java.srcDirs += 'src/main/kotlin' | ||||
|   } | ||||
|  | ||||
|   defaultConfig { | ||||
|     applicationId "app.alextran.immich" | ||||
|     minSdkVersion 26 | ||||
|     targetSdkVersion 35 | ||||
|     versionCode flutterVersionCode.toInteger() | ||||
|     versionName flutterVersionName | ||||
|   } | ||||
|  | ||||
|   signingConfigs { | ||||
|     release { | ||||
|       def keyAliasVal = System.getenv("ALIAS") | ||||
|       def keyPasswordVal = System.getenv("ANDROID_KEY_PASSWORD") | ||||
|       def storePasswordVal = System.getenv("ANDROID_STORE_PASSWORD") | ||||
|  | ||||
|       keyAlias keyAliasVal ? keyAliasVal : keystoreProperties['keyAlias'] | ||||
|       keyPassword keyPasswordVal ? keyPasswordVal : keystoreProperties['keyPassword'] | ||||
|       storeFile file("../key.jks") ? file("../key.jks") : file(keystoreProperties['storeFile']) | ||||
|       storePassword storePasswordVal ? storePasswordVal : keystoreProperties['storePassword'] | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   buildTypes { | ||||
|     debug { | ||||
|       applicationIdSuffix '.debug' | ||||
|       versionNameSuffix '-DEBUG' | ||||
|     } | ||||
|  | ||||
|     kotlinOptions { | ||||
|         jvmTarget = '17' | ||||
|     release { | ||||
|       signingConfig signingConfigs.release | ||||
|     } | ||||
|  | ||||
|     sourceSets { | ||||
|         main.java.srcDirs += 'src/main/kotlin' | ||||
|     } | ||||
|  | ||||
|     defaultConfig { | ||||
|         applicationId "app.alextran.immich" | ||||
|         minSdkVersion 26 | ||||
|         targetSdkVersion 35 | ||||
|         versionCode flutterVersionCode.toInteger() | ||||
|         versionName flutterVersionName | ||||
|     } | ||||
|  | ||||
|    signingConfigs { | ||||
|        release { | ||||
|             def keyAliasVal = System.getenv("ALIAS") | ||||
|             def keyPasswordVal = System.getenv("ANDROID_KEY_PASSWORD") | ||||
|             def storePasswordVal = System.getenv("ANDROID_STORE_PASSWORD") | ||||
|  | ||||
|             keyAlias keyAliasVal ? keyAliasVal : keystoreProperties['keyAlias'] | ||||
|             keyPassword keyPasswordVal ? keyPasswordVal : keystoreProperties['keyPassword'] | ||||
|             storeFile file("../key.jks") ? file("../key.jks") : file(keystoreProperties['storeFile']) | ||||
|             storePassword storePasswordVal ? storePasswordVal : keystoreProperties['storePassword'] | ||||
|        } | ||||
|    } | ||||
|  | ||||
|     buildTypes { | ||||
|         debug { | ||||
|             applicationIdSuffix '.debug' | ||||
|             versionNameSuffix '-DEBUG' | ||||
|         } | ||||
|  | ||||
|         release { | ||||
|             signingConfig signingConfigs.release | ||||
|         } | ||||
|     } | ||||
|     namespace 'app.alextran.immich' | ||||
|   } | ||||
|   namespace 'app.alextran.immich' | ||||
| } | ||||
|  | ||||
| flutter { | ||||
|     source '../..' | ||||
|   source '../..' | ||||
| } | ||||
|  | ||||
| dependencies { | ||||
|     def kotlin_version = '2.0.20' | ||||
|     def kotlin_coroutines_version = '1.9.0' | ||||
|     def work_version = '2.9.1' | ||||
|     def concurrent_version = '1.2.0' | ||||
|     def guava_version = '33.3.1-android' | ||||
|     def glide_version = '4.16.0' | ||||
|   def kotlin_version = '2.0.20' | ||||
|   def kotlin_coroutines_version = '1.9.0' | ||||
|   def work_version = '2.9.1' | ||||
|   def concurrent_version = '1.2.0' | ||||
|   def guava_version = '33.3.1-android' | ||||
|   def glide_version = '4.16.0' | ||||
|   def serialization_version = '1.8.1' | ||||
|  | ||||
|     implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" | ||||
|     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" | ||||
|     implementation "androidx.work:work-runtime-ktx:$work_version" | ||||
|     implementation "androidx.concurrent:concurrent-futures:$concurrent_version" | ||||
|     implementation "com.google.guava:guava:$guava_version" | ||||
|     implementation "com.github.bumptech.glide:glide:$glide_version" | ||||
|     ksp "com.github.bumptech.glide:ksp:$glide_version" | ||||
|     coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2' | ||||
|   implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" | ||||
|   implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" | ||||
|   implementation "androidx.work:work-runtime-ktx:$work_version" | ||||
|   implementation "androidx.concurrent:concurrent-futures:$concurrent_version" | ||||
|   implementation "com.google.guava:guava:$guava_version" | ||||
|   implementation "com.github.bumptech.glide:glide:$glide_version" | ||||
|   implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version" | ||||
|  | ||||
|   ksp "com.github.bumptech.glide:ksp:$glide_version" | ||||
|   coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2' | ||||
| } | ||||
|  | ||||
| // This is uncommented in F-Droid build script | ||||
|   | ||||
| @@ -1,6 +1,11 @@ | ||||
| package app.alextran.immich | ||||
|  | ||||
| import android.os.Build | ||||
| import android.os.ext.SdkExtensions | ||||
| import androidx.annotation.NonNull | ||||
| import app.alextran.immich.sync.NativeSyncApi | ||||
| import app.alextran.immich.sync.NativeSyncApiImpl26 | ||||
| import app.alextran.immich.sync.NativeSyncApiImpl30 | ||||
| import io.flutter.embedding.android.FlutterFragmentActivity | ||||
| import io.flutter.embedding.engine.FlutterEngine | ||||
|  | ||||
| @@ -10,5 +15,13 @@ class MainActivity : FlutterFragmentActivity() { | ||||
|     flutterEngine.plugins.add(BackgroundServicePlugin()) | ||||
|     flutterEngine.plugins.add(HttpSSLOptionsPlugin()) | ||||
|     // No need to set up method channel here as it's now handled in the plugin | ||||
|  | ||||
|     val nativeSyncApiImpl = | ||||
|       if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R || SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R) < 1) { | ||||
|         NativeSyncApiImpl26(this) | ||||
|       } else { | ||||
|         NativeSyncApiImpl30(this) | ||||
|       } | ||||
|     NativeSyncApi.setUp(flutterEngine.dartExecutor.binaryMessenger, nativeSyncApiImpl) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,393 @@ | ||||
| // Autogenerated from Pigeon (v25.3.2), do not edit directly. | ||||
| // See also: https://pub.dev/packages/pigeon | ||||
| @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") | ||||
|  | ||||
| package app.alextran.immich.sync | ||||
|  | ||||
| import android.util.Log | ||||
| import io.flutter.plugin.common.BasicMessageChannel | ||||
| import io.flutter.plugin.common.BinaryMessenger | ||||
| import io.flutter.plugin.common.EventChannel | ||||
| import io.flutter.plugin.common.MessageCodec | ||||
| import io.flutter.plugin.common.StandardMethodCodec | ||||
| import io.flutter.plugin.common.StandardMessageCodec | ||||
| import java.io.ByteArrayOutputStream | ||||
| import java.nio.ByteBuffer | ||||
| private object MessagesPigeonUtils { | ||||
|  | ||||
|   fun wrapResult(result: Any?): List<Any?> { | ||||
|     return listOf(result) | ||||
|   } | ||||
|  | ||||
|   fun wrapError(exception: Throwable): List<Any?> { | ||||
|     return if (exception is FlutterError) { | ||||
|       listOf( | ||||
|         exception.code, | ||||
|         exception.message, | ||||
|         exception.details | ||||
|       ) | ||||
|     } else { | ||||
|       listOf( | ||||
|         exception.javaClass.simpleName, | ||||
|         exception.toString(), | ||||
|         "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) | ||||
|       ) | ||||
|     } | ||||
|   } | ||||
|   fun deepEquals(a: Any?, b: Any?): Boolean { | ||||
|     if (a is ByteArray && b is ByteArray) { | ||||
|         return a.contentEquals(b) | ||||
|     } | ||||
|     if (a is IntArray && b is IntArray) { | ||||
|         return a.contentEquals(b) | ||||
|     } | ||||
|     if (a is LongArray && b is LongArray) { | ||||
|         return a.contentEquals(b) | ||||
|     } | ||||
|     if (a is DoubleArray && b is DoubleArray) { | ||||
|         return a.contentEquals(b) | ||||
|     } | ||||
|     if (a is Array<*> && b is Array<*>) { | ||||
|       return a.size == b.size && | ||||
|           a.indices.all{ deepEquals(a[it], b[it]) } | ||||
|     } | ||||
|     if (a is List<*> && b is List<*>) { | ||||
|       return a.size == b.size && | ||||
|           a.indices.all{ deepEquals(a[it], b[it]) } | ||||
|     } | ||||
|     if (a is Map<*, *> && b is Map<*, *>) { | ||||
|       return a.size == b.size && a.all { | ||||
|           (b as Map<Any?, Any?>).containsKey(it.key) && | ||||
|           deepEquals(it.value, b[it.key]) | ||||
|       } | ||||
|     } | ||||
|     return a == b | ||||
|   } | ||||
|        | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Error class for passing custom error details to Flutter via a thrown PlatformException. | ||||
|  * @property code The error code. | ||||
|  * @property message The error message. | ||||
|  * @property details The error details. Must be a datatype supported by the api codec. | ||||
|  */ | ||||
| class FlutterError ( | ||||
|   val code: String, | ||||
|   override val message: String? = null, | ||||
|   val details: Any? = null | ||||
| ) : Throwable() | ||||
|  | ||||
| /** Generated class from Pigeon that represents data sent in messages. */ | ||||
| data class PlatformAsset ( | ||||
|   val id: String, | ||||
|   val name: String, | ||||
|   val type: Long, | ||||
|   val createdAt: Long? = null, | ||||
|   val updatedAt: Long? = null, | ||||
|   val durationInSeconds: Long | ||||
| ) | ||||
|  { | ||||
|   companion object { | ||||
|     fun fromList(pigeonVar_list: List<Any?>): PlatformAsset { | ||||
|       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 Long? | ||||
|       val updatedAt = pigeonVar_list[4] as Long? | ||||
|       val durationInSeconds = pigeonVar_list[5] as Long | ||||
|       return PlatformAsset(id, name, type, createdAt, updatedAt, durationInSeconds) | ||||
|     } | ||||
|   } | ||||
|   fun toList(): List<Any?> { | ||||
|     return listOf( | ||||
|       id, | ||||
|       name, | ||||
|       type, | ||||
|       createdAt, | ||||
|       updatedAt, | ||||
|       durationInSeconds, | ||||
|     ) | ||||
|   } | ||||
|   override fun equals(other: Any?): Boolean { | ||||
|     if (other !is PlatformAsset) { | ||||
|       return false | ||||
|     } | ||||
|     if (this === other) { | ||||
|       return true | ||||
|     } | ||||
|     return MessagesPigeonUtils.deepEquals(toList(), other.toList())  } | ||||
|  | ||||
|   override fun hashCode(): Int = toList().hashCode() | ||||
| } | ||||
|  | ||||
| /** Generated class from Pigeon that represents data sent in messages. */ | ||||
| data class PlatformAlbum ( | ||||
|   val id: String, | ||||
|   val name: String, | ||||
|   val updatedAt: Long? = null, | ||||
|   val isCloud: Boolean, | ||||
|   val assetCount: Long | ||||
| ) | ||||
|  { | ||||
|   companion object { | ||||
|     fun fromList(pigeonVar_list: List<Any?>): PlatformAlbum { | ||||
|       val id = pigeonVar_list[0] as String | ||||
|       val name = pigeonVar_list[1] as String | ||||
|       val updatedAt = pigeonVar_list[2] as Long? | ||||
|       val isCloud = pigeonVar_list[3] as Boolean | ||||
|       val assetCount = pigeonVar_list[4] as Long | ||||
|       return PlatformAlbum(id, name, updatedAt, isCloud, assetCount) | ||||
|     } | ||||
|   } | ||||
|   fun toList(): List<Any?> { | ||||
|     return listOf( | ||||
|       id, | ||||
|       name, | ||||
|       updatedAt, | ||||
|       isCloud, | ||||
|       assetCount, | ||||
|     ) | ||||
|   } | ||||
|   override fun equals(other: Any?): Boolean { | ||||
|     if (other !is PlatformAlbum) { | ||||
|       return false | ||||
|     } | ||||
|     if (this === other) { | ||||
|       return true | ||||
|     } | ||||
|     return MessagesPigeonUtils.deepEquals(toList(), other.toList())  } | ||||
|  | ||||
|   override fun hashCode(): Int = toList().hashCode() | ||||
| } | ||||
|  | ||||
| /** Generated class from Pigeon that represents data sent in messages. */ | ||||
| data class SyncDelta ( | ||||
|   val hasChanges: Boolean, | ||||
|   val updates: List<PlatformAsset>, | ||||
|   val deletes: List<String>, | ||||
|   val assetAlbums: Map<String, List<String>> | ||||
| ) | ||||
|  { | ||||
|   companion object { | ||||
|     fun fromList(pigeonVar_list: List<Any?>): SyncDelta { | ||||
|       val hasChanges = pigeonVar_list[0] as Boolean | ||||
|       val updates = pigeonVar_list[1] as List<PlatformAsset> | ||||
|       val deletes = pigeonVar_list[2] as List<String> | ||||
|       val assetAlbums = pigeonVar_list[3] as Map<String, List<String>> | ||||
|       return SyncDelta(hasChanges, updates, deletes, assetAlbums) | ||||
|     } | ||||
|   } | ||||
|   fun toList(): List<Any?> { | ||||
|     return listOf( | ||||
|       hasChanges, | ||||
|       updates, | ||||
|       deletes, | ||||
|       assetAlbums, | ||||
|     ) | ||||
|   } | ||||
|   override fun equals(other: Any?): Boolean { | ||||
|     if (other !is SyncDelta) { | ||||
|       return false | ||||
|     } | ||||
|     if (this === other) { | ||||
|       return true | ||||
|     } | ||||
|     return MessagesPigeonUtils.deepEquals(toList(), other.toList())  } | ||||
|  | ||||
|   override fun hashCode(): Int = toList().hashCode() | ||||
| } | ||||
| private open class MessagesPigeonCodec : StandardMessageCodec() { | ||||
|   override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { | ||||
|     return when (type) { | ||||
|       129.toByte() -> { | ||||
|         return (readValue(buffer) as? List<Any?>)?.let { | ||||
|           PlatformAsset.fromList(it) | ||||
|         } | ||||
|       } | ||||
|       130.toByte() -> { | ||||
|         return (readValue(buffer) as? List<Any?>)?.let { | ||||
|           PlatformAlbum.fromList(it) | ||||
|         } | ||||
|       } | ||||
|       131.toByte() -> { | ||||
|         return (readValue(buffer) as? List<Any?>)?.let { | ||||
|           SyncDelta.fromList(it) | ||||
|         } | ||||
|       } | ||||
|       else -> super.readValueOfType(type, buffer) | ||||
|     } | ||||
|   } | ||||
|   override fun writeValue(stream: ByteArrayOutputStream, value: Any?)   { | ||||
|     when (value) { | ||||
|       is PlatformAsset -> { | ||||
|         stream.write(129) | ||||
|         writeValue(stream, value.toList()) | ||||
|       } | ||||
|       is PlatformAlbum -> { | ||||
|         stream.write(130) | ||||
|         writeValue(stream, value.toList()) | ||||
|       } | ||||
|       is SyncDelta -> { | ||||
|         stream.write(131) | ||||
|         writeValue(stream, value.toList()) | ||||
|       } | ||||
|       else -> super.writeValue(stream, value) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ | ||||
| interface NativeSyncApi { | ||||
|   fun shouldFullSync(): Boolean | ||||
|   fun getMediaChanges(): SyncDelta | ||||
|   fun checkpointSync() | ||||
|   fun clearSyncCheckpoint() | ||||
|   fun getAssetIdsForAlbum(albumId: String): List<String> | ||||
|   fun getAlbums(): List<PlatformAlbum> | ||||
|   fun getAssetsCountSince(albumId: String, timestamp: Long): Long | ||||
|   fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<PlatformAsset> | ||||
|  | ||||
|   companion object { | ||||
|     /** The codec used by NativeSyncApi. */ | ||||
|     val codec: MessageCodec<Any?> by lazy { | ||||
|       MessagesPigeonCodec() | ||||
|     } | ||||
|     /** Sets up an instance of `NativeSyncApi` to handle messages through the `binaryMessenger`. */ | ||||
|     @JvmOverloads | ||||
|     fun setUp(binaryMessenger: BinaryMessenger, api: NativeSyncApi?, messageChannelSuffix: String = "") { | ||||
|       val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" | ||||
|       val taskQueue = binaryMessenger.makeBackgroundTaskQueue() | ||||
|       run { | ||||
|         val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$separatedMessageChannelSuffix", codec) | ||||
|         if (api != null) { | ||||
|           channel.setMessageHandler { _, reply -> | ||||
|             val wrapped: List<Any?> = try { | ||||
|               listOf(api.shouldFullSync()) | ||||
|             } catch (exception: Throwable) { | ||||
|               MessagesPigeonUtils.wrapError(exception) | ||||
|             } | ||||
|             reply.reply(wrapped) | ||||
|           } | ||||
|         } else { | ||||
|           channel.setMessageHandler(null) | ||||
|         } | ||||
|       } | ||||
|       run { | ||||
|         val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$separatedMessageChannelSuffix", codec, taskQueue) | ||||
|         if (api != null) { | ||||
|           channel.setMessageHandler { _, reply -> | ||||
|             val wrapped: List<Any?> = try { | ||||
|               listOf(api.getMediaChanges()) | ||||
|             } catch (exception: Throwable) { | ||||
|               MessagesPigeonUtils.wrapError(exception) | ||||
|             } | ||||
|             reply.reply(wrapped) | ||||
|           } | ||||
|         } else { | ||||
|           channel.setMessageHandler(null) | ||||
|         } | ||||
|       } | ||||
|       run { | ||||
|         val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync$separatedMessageChannelSuffix", codec) | ||||
|         if (api != null) { | ||||
|           channel.setMessageHandler { _, reply -> | ||||
|             val wrapped: List<Any?> = try { | ||||
|               api.checkpointSync() | ||||
|               listOf(null) | ||||
|             } catch (exception: Throwable) { | ||||
|               MessagesPigeonUtils.wrapError(exception) | ||||
|             } | ||||
|             reply.reply(wrapped) | ||||
|           } | ||||
|         } else { | ||||
|           channel.setMessageHandler(null) | ||||
|         } | ||||
|       } | ||||
|       run { | ||||
|         val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint$separatedMessageChannelSuffix", codec) | ||||
|         if (api != null) { | ||||
|           channel.setMessageHandler { _, reply -> | ||||
|             val wrapped: List<Any?> = try { | ||||
|               api.clearSyncCheckpoint() | ||||
|               listOf(null) | ||||
|             } catch (exception: Throwable) { | ||||
|               MessagesPigeonUtils.wrapError(exception) | ||||
|             } | ||||
|             reply.reply(wrapped) | ||||
|           } | ||||
|         } else { | ||||
|           channel.setMessageHandler(null) | ||||
|         } | ||||
|       } | ||||
|       run { | ||||
|         val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum$separatedMessageChannelSuffix", codec, taskQueue) | ||||
|         if (api != null) { | ||||
|           channel.setMessageHandler { message, reply -> | ||||
|             val args = message as List<Any?> | ||||
|             val albumIdArg = args[0] as String | ||||
|             val wrapped: List<Any?> = try { | ||||
|               listOf(api.getAssetIdsForAlbum(albumIdArg)) | ||||
|             } catch (exception: Throwable) { | ||||
|               MessagesPigeonUtils.wrapError(exception) | ||||
|             } | ||||
|             reply.reply(wrapped) | ||||
|           } | ||||
|         } else { | ||||
|           channel.setMessageHandler(null) | ||||
|         } | ||||
|       } | ||||
|       run { | ||||
|         val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$separatedMessageChannelSuffix", codec, taskQueue) | ||||
|         if (api != null) { | ||||
|           channel.setMessageHandler { _, reply -> | ||||
|             val wrapped: List<Any?> = try { | ||||
|               listOf(api.getAlbums()) | ||||
|             } catch (exception: Throwable) { | ||||
|               MessagesPigeonUtils.wrapError(exception) | ||||
|             } | ||||
|             reply.reply(wrapped) | ||||
|           } | ||||
|         } else { | ||||
|           channel.setMessageHandler(null) | ||||
|         } | ||||
|       } | ||||
|       run { | ||||
|         val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince$separatedMessageChannelSuffix", codec, taskQueue) | ||||
|         if (api != null) { | ||||
|           channel.setMessageHandler { message, reply -> | ||||
|             val args = message as List<Any?> | ||||
|             val albumIdArg = args[0] as String | ||||
|             val timestampArg = args[1] as Long | ||||
|             val wrapped: List<Any?> = try { | ||||
|               listOf(api.getAssetsCountSince(albumIdArg, timestampArg)) | ||||
|             } catch (exception: Throwable) { | ||||
|               MessagesPigeonUtils.wrapError(exception) | ||||
|             } | ||||
|             reply.reply(wrapped) | ||||
|           } | ||||
|         } else { | ||||
|           channel.setMessageHandler(null) | ||||
|         } | ||||
|       } | ||||
|       run { | ||||
|         val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$separatedMessageChannelSuffix", codec, taskQueue) | ||||
|         if (api != null) { | ||||
|           channel.setMessageHandler { message, reply -> | ||||
|             val args = message as List<Any?> | ||||
|             val albumIdArg = args[0] as String | ||||
|             val updatedTimeCondArg = args[1] as Long? | ||||
|             val wrapped: List<Any?> = try { | ||||
|               listOf(api.getAssetsForAlbum(albumIdArg, updatedTimeCondArg)) | ||||
|             } catch (exception: Throwable) { | ||||
|               MessagesPigeonUtils.wrapError(exception) | ||||
|             } | ||||
|             reply.reply(wrapped) | ||||
|           } | ||||
|         } else { | ||||
|           channel.setMessageHandler(null) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,24 @@ | ||||
| package app.alextran.immich.sync | ||||
|  | ||||
| import android.content.Context | ||||
|  | ||||
|  | ||||
| class NativeSyncApiImpl26(context: Context) : NativeSyncApiImplBase(context), NativeSyncApi { | ||||
|   override fun shouldFullSync(): Boolean { | ||||
|     return true | ||||
|   } | ||||
|  | ||||
|   // No-op for Android 10 and below | ||||
|   override fun checkpointSync() { | ||||
|     // Cannot throw exception as this is called from the Dart side | ||||
|     // during the full sync process as well | ||||
|   } | ||||
|  | ||||
|   override fun clearSyncCheckpoint() { | ||||
|     // No-op for Android 10 and below | ||||
|   } | ||||
|  | ||||
|   override fun getMediaChanges(): SyncDelta { | ||||
|     throw IllegalStateException("Method not supported on this Android version.") | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,89 @@ | ||||
| package app.alextran.immich.sync | ||||
|  | ||||
| import android.content.Context | ||||
| import android.os.Build | ||||
| import android.provider.MediaStore | ||||
| import androidx.annotation.RequiresApi | ||||
| import androidx.annotation.RequiresExtension | ||||
| import kotlinx.serialization.json.Json | ||||
|  | ||||
| @RequiresApi(Build.VERSION_CODES.Q) | ||||
| @RequiresExtension(extension = Build.VERSION_CODES.R, version = 1) | ||||
| class NativeSyncApiImpl30(context: Context) : NativeSyncApiImplBase(context), NativeSyncApi { | ||||
|   private val ctx: Context = context.applicationContext | ||||
|   private val prefs = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) | ||||
|  | ||||
|   companion object { | ||||
|     const val SHARED_PREF_NAME = "Immich::MediaManager" | ||||
|     const val SHARED_PREF_MEDIA_STORE_VERSION_KEY = "MediaStore::getVersion" | ||||
|     const val SHARED_PREF_MEDIA_STORE_GEN_KEY = "MediaStore::getGeneration" | ||||
|   } | ||||
|  | ||||
|   private fun getSavedGenerationMap(): Map<String, Long> { | ||||
|     return prefs.getString(SHARED_PREF_MEDIA_STORE_GEN_KEY, null)?.let { | ||||
|       Json.decodeFromString<Map<String, Long>>(it) | ||||
|     } ?: emptyMap() | ||||
|   } | ||||
|  | ||||
|   override fun clearSyncCheckpoint() { | ||||
|     prefs.edit().apply { | ||||
|       remove(SHARED_PREF_MEDIA_STORE_VERSION_KEY) | ||||
|       remove(SHARED_PREF_MEDIA_STORE_GEN_KEY) | ||||
|       apply() | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   override fun shouldFullSync(): Boolean = | ||||
|     MediaStore.getVersion(ctx) != prefs.getString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, null) | ||||
|  | ||||
|   override fun checkpointSync() { | ||||
|     val genMap = MediaStore.getExternalVolumeNames(ctx) | ||||
|       .associateWith { MediaStore.getGeneration(ctx, it) } | ||||
|  | ||||
|     prefs.edit().apply { | ||||
|       putString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, MediaStore.getVersion(ctx)) | ||||
|       putString(SHARED_PREF_MEDIA_STORE_GEN_KEY, Json.encodeToString(genMap)) | ||||
|       apply() | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   override fun getMediaChanges(): SyncDelta { | ||||
|     val genMap = getSavedGenerationMap() | ||||
|     val currentVolumes = MediaStore.getExternalVolumeNames(ctx) | ||||
|     val changed = mutableListOf<PlatformAsset>() | ||||
|     val deleted = mutableListOf<String>() | ||||
|     val assetAlbums = mutableMapOf<String, List<String>>() | ||||
|     var hasChanges = genMap.keys != currentVolumes | ||||
|  | ||||
|     for (volume in currentVolumes) { | ||||
|       val currentGen = MediaStore.getGeneration(ctx, volume) | ||||
|       val storedGen = genMap[volume] ?: 0 | ||||
|       if (currentGen <= storedGen) { | ||||
|         continue | ||||
|       } | ||||
|  | ||||
|       hasChanges = true | ||||
|  | ||||
|       val selection = | ||||
|         "$MEDIA_SELECTION AND (${MediaStore.MediaColumns.GENERATION_MODIFIED} > ? OR ${MediaStore.MediaColumns.GENERATION_ADDED} > ?)" | ||||
|       val selectionArgs = arrayOf( | ||||
|         *MEDIA_SELECTION_ARGS, | ||||
|         storedGen.toString(), | ||||
|         storedGen.toString() | ||||
|       ) | ||||
|  | ||||
|       getAssets(getCursor(volume, selection, selectionArgs)).forEach { | ||||
|         when (it) { | ||||
|           is AssetResult.ValidAsset -> { | ||||
|             changed.add(it.asset) | ||||
|             assetAlbums[it.asset.id] = listOf(it.albumId) | ||||
|           } | ||||
|  | ||||
|           is AssetResult.InvalidAsset -> deleted.add(it.assetId) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     // Unmounted volumes are handled in dart when the album is removed | ||||
|     return SyncDelta(hasChanges, changed, deleted, assetAlbums) | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,177 @@ | ||||
| package app.alextran.immich.sync | ||||
|  | ||||
| import android.annotation.SuppressLint | ||||
| import android.content.Context | ||||
| import android.database.Cursor | ||||
| import android.provider.MediaStore | ||||
| import java.io.File | ||||
|  | ||||
| sealed class AssetResult { | ||||
|   data class ValidAsset(val asset: PlatformAsset, val albumId: String) : AssetResult() | ||||
|   data class InvalidAsset(val assetId: String) : AssetResult() | ||||
| } | ||||
|  | ||||
| @SuppressLint("InlinedApi") | ||||
| open class NativeSyncApiImplBase(context: Context) { | ||||
|   private val ctx: Context = context.applicationContext | ||||
|  | ||||
|   companion object { | ||||
|     const val MEDIA_SELECTION = | ||||
|       "(${MediaStore.Files.FileColumns.MEDIA_TYPE} = ? OR ${MediaStore.Files.FileColumns.MEDIA_TYPE} = ?)" | ||||
|     val MEDIA_SELECTION_ARGS = arrayOf( | ||||
|       MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString(), | ||||
|       MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString() | ||||
|     ) | ||||
|     const val BUCKET_SELECTION = "(${MediaStore.Files.FileColumns.BUCKET_ID} = ?)" | ||||
|     val ASSET_PROJECTION = arrayOf( | ||||
|       MediaStore.MediaColumns._ID, | ||||
|       MediaStore.MediaColumns.DATA, | ||||
|       MediaStore.MediaColumns.DISPLAY_NAME, | ||||
|       MediaStore.MediaColumns.DATE_TAKEN, | ||||
|       MediaStore.MediaColumns.DATE_ADDED, | ||||
|       MediaStore.MediaColumns.DATE_MODIFIED, | ||||
|       MediaStore.Files.FileColumns.MEDIA_TYPE, | ||||
|       MediaStore.MediaColumns.BUCKET_ID, | ||||
|       MediaStore.MediaColumns.DURATION | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   protected fun getCursor( | ||||
|     volume: String, | ||||
|     selection: String, | ||||
|     selectionArgs: Array<String>, | ||||
|     projection: Array<String> = ASSET_PROJECTION, | ||||
|     sortOrder: String? = null | ||||
|   ): Cursor? = ctx.contentResolver.query( | ||||
|     MediaStore.Files.getContentUri(volume), | ||||
|     projection, | ||||
|     selection, | ||||
|     selectionArgs, | ||||
|     sortOrder, | ||||
|   ) | ||||
|  | ||||
|   protected fun getAssets(cursor: Cursor?): Sequence<AssetResult> { | ||||
|     return sequence { | ||||
|       cursor?.use { c -> | ||||
|         val idColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID) | ||||
|         val dataColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA) | ||||
|         val nameColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME) | ||||
|         val dateTakenColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_TAKEN) | ||||
|         val dateAddedColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED) | ||||
|         val dateModifiedColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED) | ||||
|         val mediaTypeColumn = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE) | ||||
|         val bucketIdColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.BUCKET_ID) | ||||
|         val durationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION) | ||||
|  | ||||
|         while (c.moveToNext()) { | ||||
|           val id = c.getLong(idColumn).toString() | ||||
|  | ||||
|           val path = c.getString(dataColumn) | ||||
|           if (path.isNullOrBlank() || !File(path).exists()) { | ||||
|             yield(AssetResult.InvalidAsset(id)) | ||||
|             continue | ||||
|           } | ||||
|  | ||||
|           val mediaType = c.getInt(mediaTypeColumn) | ||||
|           val name = c.getString(nameColumn) | ||||
|           // Date taken is milliseconds since epoch, Date added is seconds since epoch | ||||
|           val createdAt = (c.getLong(dateTakenColumn).takeIf { it > 0 }?.div(1000)) | ||||
|             ?: c.getLong(dateAddedColumn) | ||||
|           // Date modified is seconds since epoch | ||||
|           val modifiedAt = c.getLong(dateModifiedColumn) | ||||
|           // Duration is milliseconds | ||||
|           val duration = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0 | ||||
|           else c.getLong(durationColumn) / 1000 | ||||
|           val bucketId = c.getString(bucketIdColumn) | ||||
|  | ||||
|           val asset = PlatformAsset(id, name, mediaType.toLong(), createdAt, modifiedAt, duration) | ||||
|           yield(AssetResult.ValidAsset(asset, bucketId)) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   fun getAlbums(): List<PlatformAlbum> { | ||||
|     val albums = mutableListOf<PlatformAlbum>() | ||||
|     val albumsCount = mutableMapOf<String, Int>() | ||||
|  | ||||
|     val projection = arrayOf( | ||||
|       MediaStore.Files.FileColumns.BUCKET_ID, | ||||
|       MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME, | ||||
|       MediaStore.Files.FileColumns.DATE_MODIFIED, | ||||
|     ) | ||||
|     val selection = | ||||
|       "(${MediaStore.Files.FileColumns.BUCKET_ID} IS NOT NULL) AND $MEDIA_SELECTION" | ||||
|  | ||||
|     getCursor( | ||||
|       MediaStore.VOLUME_EXTERNAL, | ||||
|       selection, | ||||
|       MEDIA_SELECTION_ARGS, | ||||
|       projection, | ||||
|       "${MediaStore.Files.FileColumns.DATE_MODIFIED} DESC" | ||||
|     )?.use { cursor -> | ||||
|       val bucketIdColumn = | ||||
|         cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.BUCKET_ID) | ||||
|       val bucketNameColumn = | ||||
|         cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME) | ||||
|       val dateModified = | ||||
|         cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_MODIFIED) | ||||
|  | ||||
|       while (cursor.moveToNext()) { | ||||
|         val id = cursor.getString(bucketIdColumn) | ||||
|  | ||||
|         val count = albumsCount.getOrDefault(id, 0) | ||||
|         if (count != 0) { | ||||
|           albumsCount[id] = count + 1 | ||||
|           continue | ||||
|         } | ||||
|  | ||||
|         val name = cursor.getString(bucketNameColumn) | ||||
|         val updatedAt = cursor.getLong(dateModified) | ||||
|         albums.add(PlatformAlbum(id, name, updatedAt, false, 0)) | ||||
|         albumsCount[id] = 1 | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return albums.map { it.copy(assetCount = albumsCount[it.id]?.toLong() ?: 0) } | ||||
|       .sortedBy { it.id } | ||||
|   } | ||||
|  | ||||
|   fun getAssetIdsForAlbum(albumId: String): List<String> { | ||||
|     val projection = arrayOf(MediaStore.MediaColumns._ID) | ||||
|  | ||||
|     return getCursor( | ||||
|       MediaStore.VOLUME_EXTERNAL, | ||||
|       "$BUCKET_SELECTION AND $MEDIA_SELECTION", | ||||
|       arrayOf(albumId, *MEDIA_SELECTION_ARGS), | ||||
|       projection | ||||
|     )?.use { cursor -> | ||||
|       val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID) | ||||
|       generateSequence { | ||||
|         if (cursor.moveToNext()) cursor.getLong(idColumn).toString() else null | ||||
|       }.toList() | ||||
|     } ?: emptyList() | ||||
|   } | ||||
|  | ||||
|   fun getAssetsCountSince(albumId: String, timestamp: Long): Long = | ||||
|     getCursor( | ||||
|       MediaStore.VOLUME_EXTERNAL, | ||||
|       "$BUCKET_SELECTION AND ${MediaStore.Files.FileColumns.DATE_ADDED} > ? AND $MEDIA_SELECTION", | ||||
|       arrayOf(albumId, timestamp.toString(), *MEDIA_SELECTION_ARGS), | ||||
|     )?.use { cursor -> cursor.count.toLong() } ?: 0L | ||||
|  | ||||
|  | ||||
|   fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<PlatformAsset> { | ||||
|     var selection = "$BUCKET_SELECTION AND $MEDIA_SELECTION" | ||||
|     val selectionArgs = mutableListOf(albumId, *MEDIA_SELECTION_ARGS) | ||||
|  | ||||
|     if (updatedTimeCond != null) { | ||||
|       selection += " AND (${MediaStore.Files.FileColumns.DATE_MODIFIED} > ? OR ${MediaStore.Files.FileColumns.DATE_ADDED} > ?)" | ||||
|       selectionArgs.addAll(listOf(updatedTimeCond.toString(), updatedTimeCond.toString())) | ||||
|     } | ||||
|  | ||||
|     return getAssets(getCursor(MediaStore.VOLUME_EXTERNAL, selection, selectionArgs.toTypedArray())) | ||||
|       .mapNotNull { result -> (result as? AssetResult.ValidAsset)?.asset } | ||||
|       .toList() | ||||
|   } | ||||
| } | ||||
| @@ -1,26 +1,27 @@ | ||||
| pluginManagement { | ||||
|     def flutterSdkPath = { | ||||
|         def properties = new Properties() | ||||
|         file("local.properties").withInputStream { properties.load(it) } | ||||
|         def flutterSdkPath = properties.getProperty("flutter.sdk") | ||||
|         assert flutterSdkPath != null, "flutter.sdk not set in local.properties" | ||||
|         return flutterSdkPath | ||||
|     }() | ||||
|   def flutterSdkPath = { | ||||
|     def properties = new Properties() | ||||
|     file("local.properties").withInputStream { properties.load(it) } | ||||
|     def flutterSdkPath = properties.getProperty("flutter.sdk") | ||||
|     assert flutterSdkPath != null, "flutter.sdk not set in local.properties" | ||||
|     return flutterSdkPath | ||||
|   }() | ||||
|  | ||||
|     includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") | ||||
|   includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") | ||||
|  | ||||
|     repositories { | ||||
|         google() | ||||
|         mavenCentral() | ||||
|         gradlePluginPortal() | ||||
|     } | ||||
|   repositories { | ||||
|     google() | ||||
|     mavenCentral() | ||||
|     gradlePluginPortal() | ||||
|   } | ||||
| } | ||||
|  | ||||
| plugins { | ||||
|     id "dev.flutter.flutter-plugin-loader" version "1.0.0" | ||||
|     id "com.android.application" version '8.7.2' apply false | ||||
|     id "org.jetbrains.kotlin.android" version "2.0.20" apply false | ||||
|     id 'com.google.devtools.ksp' version '2.0.20-1.0.24' apply false | ||||
|   id "dev.flutter.flutter-plugin-loader" version "1.0.0" | ||||
|   id "com.android.application" version '8.7.2' apply false | ||||
|   id "org.jetbrains.kotlin.android" version "2.0.20" apply false | ||||
|   id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.22' apply false | ||||
|   id 'com.google.devtools.ksp' version '2.0.20-1.0.24' apply false | ||||
| } | ||||
|  | ||||
| include ":app" | ||||
|   | ||||
| @@ -5,31 +5,26 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: _fe_analyzer_shared | ||||
|       sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" | ||||
|       sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "76.0.0" | ||||
|   _macros: | ||||
|     dependency: transitive | ||||
|     description: dart | ||||
|     source: sdk | ||||
|     version: "0.3.3" | ||||
|     version: "80.0.0" | ||||
|   analyzer: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: analyzer | ||||
|       sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" | ||||
|       sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "6.11.0" | ||||
|     version: "7.3.0" | ||||
|   analyzer_plugin: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: analyzer_plugin | ||||
|       sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" | ||||
|       sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.11.3" | ||||
|     version: "0.13.0" | ||||
|   args: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -106,34 +101,42 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: custom_lint | ||||
|       sha256: "4500e88854e7581ee43586abeaf4443cb22375d6d289241a87b1aadf678d5545" | ||||
|       sha256: "409c485fd14f544af1da965d5a0d160ee57cd58b63eeaa7280a4f28cf5bda7f1" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.6.10" | ||||
|     version: "0.7.5" | ||||
|   custom_lint_builder: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: custom_lint_builder | ||||
|       sha256: "5a95eff100da256fbf086b329c17c8b49058c261cdf56d3a4157d3c31c511d78" | ||||
|       sha256: "107e0a43606138015777590ee8ce32f26ba7415c25b722ff0908a6f5d7a4c228" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.6.10" | ||||
|     version: "0.7.5" | ||||
|   custom_lint_core: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: custom_lint_core | ||||
|       sha256: "76a4046cc71d976222a078a8fd4a65e198b70545a8d690a75196dd14f08510f6" | ||||
|       sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.6.10" | ||||
|     version: "0.7.5" | ||||
|   custom_lint_visitor: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: custom_lint_visitor | ||||
|       sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.0.0+7.3.0" | ||||
|   dart_style: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: dart_style | ||||
|       sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" | ||||
|       sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.8" | ||||
|     version: "3.1.0" | ||||
|   file: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -154,10 +157,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: freezed_annotation | ||||
|       sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 | ||||
|       sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.4.4" | ||||
|     version: "3.0.0" | ||||
|   glob: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -198,14 +201,6 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.3.0" | ||||
|   macros: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: macros | ||||
|       sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.1.3-main.0" | ||||
|   matcher: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -367,4 +362,4 @@ packages: | ||||
|     source: hosted | ||||
|     version: "3.1.3" | ||||
| sdks: | ||||
|   dart: ">=3.6.0 <4.0.0" | ||||
|   dart: ">=3.7.0 <4.0.0" | ||||
|   | ||||
| @@ -5,9 +5,9 @@ environment: | ||||
|   sdk: '>=3.0.0 <4.0.0' | ||||
|  | ||||
| dependencies: | ||||
|   analyzer: ^6.0.0 | ||||
|   analyzer_plugin: ^0.11.3 | ||||
|   custom_lint_builder: ^0.6.4 | ||||
|   analyzer: ^7.0.0 | ||||
|   analyzer_plugin: ^0.13.0 | ||||
|   custom_lint_builder: ^0.7.5 | ||||
|   glob: ^2.1.2 | ||||
|  | ||||
| dev_dependencies: | ||||
|   | ||||
| @@ -89,6 +89,16 @@ | ||||
| 		FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = "<group>"; }; | ||||
| /* End PBXFileReference section */ | ||||
|  | ||||
| /* Begin PBXFileSystemSynchronizedRootGroup section */ | ||||
| 		B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = { | ||||
| 			isa = PBXFileSystemSynchronizedRootGroup; | ||||
| 			exceptions = ( | ||||
| 			); | ||||
| 			path = Sync; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| /* End PBXFileSystemSynchronizedRootGroup section */ | ||||
|  | ||||
| /* Begin PBXFrameworksBuildPhase section */ | ||||
| 		97C146EB1CF9000F007C117D /* Frameworks */ = { | ||||
| 			isa = PBXFrameworksBuildPhase; | ||||
| @@ -175,6 +185,7 @@ | ||||
| 		97C146F01CF9000F007C117D /* Runner */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				B2CF7F8C2DDE4EBB00744BF6 /* Sync */, | ||||
| 				FA9973382CF6DF4B000EF859 /* Runner.entitlements */, | ||||
| 				65DD438629917FAD0047FFA8 /* BackgroundSync */, | ||||
| 				FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */, | ||||
| @@ -224,6 +235,9 @@ | ||||
| 			dependencies = ( | ||||
| 				FAC6F8992D287C890078CB2F /* PBXTargetDependency */, | ||||
| 			); | ||||
| 			fileSystemSynchronizedGroups = ( | ||||
| 				B2CF7F8C2DDE4EBB00744BF6 /* Sync */, | ||||
| 			); | ||||
| 			name = Runner; | ||||
| 			productName = Runner; | ||||
| 			productReference = 97C146EE1CF9000F007C117D /* Immich-Debug.app */; | ||||
| @@ -270,7 +284,6 @@ | ||||
| 				}; | ||||
| 			}; | ||||
| 			buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; | ||||
| 			compatibilityVersion = "Xcode 9.3"; | ||||
| 			developmentRegion = en; | ||||
| 			hasScannedForEncodings = 0; | ||||
| 			knownRegions = ( | ||||
| @@ -278,6 +291,7 @@ | ||||
| 				Base, | ||||
| 			); | ||||
| 			mainGroup = 97C146E51CF9000F007C117D; | ||||
| 			preferredProjectObjectVersion = 77; | ||||
| 			productRefGroup = 97C146EF1CF9000F007C117D /* Products */; | ||||
| 			projectDirPath = ""; | ||||
| 			projectRoot = ""; | ||||
|   | ||||
| @@ -22,6 +22,9 @@ import UIKit | ||||
|     BackgroundServicePlugin.registerBackgroundProcessing() | ||||
|  | ||||
|     BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!) | ||||
|      | ||||
|     let controller: FlutterViewController = window?.rootViewController as! FlutterViewController | ||||
|     NativeSyncApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: NativeSyncApiImpl()) | ||||
|  | ||||
|     BackgroundServicePlugin.setPluginRegistrantCallback { registry in | ||||
|       if !registry.hasPlugin("org.cocoapods.path-provider-foundation") { | ||||
|   | ||||
							
								
								
									
										446
									
								
								mobile/ios/Runner/Sync/Messages.g.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										446
									
								
								mobile/ios/Runner/Sync/Messages.g.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,446 @@ | ||||
| // Autogenerated from Pigeon (v25.3.2), do not edit directly. | ||||
| // See also: https://pub.dev/packages/pigeon | ||||
|  | ||||
| import Foundation | ||||
|  | ||||
| #if os(iOS) | ||||
|   import Flutter | ||||
| #elseif os(macOS) | ||||
|   import FlutterMacOS | ||||
| #else | ||||
|   #error("Unsupported platform.") | ||||
| #endif | ||||
|  | ||||
| /// Error class for passing custom error details to Dart side. | ||||
| final class PigeonError: Error { | ||||
|   let code: String | ||||
|   let message: String? | ||||
|   let details: Sendable? | ||||
|  | ||||
|   init(code: String, message: String?, details: Sendable?) { | ||||
|     self.code = code | ||||
|     self.message = message | ||||
|     self.details = details | ||||
|   } | ||||
|  | ||||
|   var localizedDescription: String { | ||||
|     return | ||||
|       "PigeonError(code: \(code), message: \(message ?? "<nil>"), details: \(details ?? "<nil>")" | ||||
|   } | ||||
| } | ||||
|  | ||||
| private func wrapResult(_ result: Any?) -> [Any?] { | ||||
|   return [result] | ||||
| } | ||||
|  | ||||
| private func wrapError(_ error: Any) -> [Any?] { | ||||
|   if let pigeonError = error as? PigeonError { | ||||
|     return [ | ||||
|       pigeonError.code, | ||||
|       pigeonError.message, | ||||
|       pigeonError.details, | ||||
|     ] | ||||
|   } | ||||
|   if let flutterError = error as? FlutterError { | ||||
|     return [ | ||||
|       flutterError.code, | ||||
|       flutterError.message, | ||||
|       flutterError.details, | ||||
|     ] | ||||
|   } | ||||
|   return [ | ||||
|     "\(error)", | ||||
|     "\(type(of: error))", | ||||
|     "Stacktrace: \(Thread.callStackSymbols)", | ||||
|   ] | ||||
| } | ||||
|  | ||||
| private func isNullish(_ value: Any?) -> Bool { | ||||
|   return value is NSNull || value == nil | ||||
| } | ||||
|  | ||||
| private func nilOrValue<T>(_ value: Any?) -> T? { | ||||
|   if value is NSNull { return nil } | ||||
|   return value as! T? | ||||
| } | ||||
|  | ||||
| func deepEqualsMessages(_ lhs: Any?, _ rhs: Any?) -> Bool { | ||||
|   let cleanLhs = nilOrValue(lhs) as Any? | ||||
|   let cleanRhs = nilOrValue(rhs) as Any? | ||||
|   switch (cleanLhs, cleanRhs) { | ||||
|   case (nil, nil): | ||||
|     return true | ||||
|  | ||||
|   case (nil, _), (_, nil): | ||||
|     return false | ||||
|  | ||||
|   case is (Void, Void): | ||||
|     return true | ||||
|  | ||||
|   case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable): | ||||
|     return cleanLhsHashable == cleanRhsHashable | ||||
|  | ||||
|   case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]): | ||||
|     guard cleanLhsArray.count == cleanRhsArray.count else { return false } | ||||
|     for (index, element) in cleanLhsArray.enumerated() { | ||||
|       if !deepEqualsMessages(element, cleanRhsArray[index]) { | ||||
|         return false | ||||
|       } | ||||
|     } | ||||
|     return true | ||||
|  | ||||
|   case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): | ||||
|     guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false } | ||||
|     for (key, cleanLhsValue) in cleanLhsDictionary { | ||||
|       guard cleanRhsDictionary.index(forKey: key) != nil else { return false } | ||||
|       if !deepEqualsMessages(cleanLhsValue, cleanRhsDictionary[key]!) { | ||||
|         return false | ||||
|       } | ||||
|     } | ||||
|     return true | ||||
|  | ||||
|   default: | ||||
|     // Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue. | ||||
|     return false | ||||
|   } | ||||
| } | ||||
|  | ||||
| func deepHashMessages(value: Any?, hasher: inout Hasher) { | ||||
|   if let valueList = value as? [AnyHashable] { | ||||
|      for item in valueList { deepHashMessages(value: item, hasher: &hasher) } | ||||
|      return | ||||
|   } | ||||
|  | ||||
|   if let valueDict = value as? [AnyHashable: AnyHashable] { | ||||
|     for key in valueDict.keys {  | ||||
|       hasher.combine(key) | ||||
|       deepHashMessages(value: valueDict[key]!, hasher: &hasher) | ||||
|     } | ||||
|     return | ||||
|   } | ||||
|  | ||||
|   if let hashableValue = value as? AnyHashable { | ||||
|     hasher.combine(hashableValue.hashValue) | ||||
|   } | ||||
|  | ||||
|   return hasher.combine(String(describing: value)) | ||||
| } | ||||
|  | ||||
|      | ||||
|  | ||||
| /// Generated class from Pigeon that represents data sent in messages. | ||||
| struct PlatformAsset: Hashable { | ||||
|   var id: String | ||||
|   var name: String | ||||
|   var type: Int64 | ||||
|   var createdAt: Int64? = nil | ||||
|   var updatedAt: Int64? = nil | ||||
|   var durationInSeconds: Int64 | ||||
|  | ||||
|  | ||||
|   // swift-format-ignore: AlwaysUseLowerCamelCase | ||||
|   static func fromList(_ pigeonVar_list: [Any?]) -> PlatformAsset? { | ||||
|     let id = pigeonVar_list[0] as! String | ||||
|     let name = pigeonVar_list[1] as! String | ||||
|     let type = pigeonVar_list[2] as! Int64 | ||||
|     let createdAt: Int64? = nilOrValue(pigeonVar_list[3]) | ||||
|     let updatedAt: Int64? = nilOrValue(pigeonVar_list[4]) | ||||
|     let durationInSeconds = pigeonVar_list[5] as! Int64 | ||||
|  | ||||
|     return PlatformAsset( | ||||
|       id: id, | ||||
|       name: name, | ||||
|       type: type, | ||||
|       createdAt: createdAt, | ||||
|       updatedAt: updatedAt, | ||||
|       durationInSeconds: durationInSeconds | ||||
|     ) | ||||
|   } | ||||
|   func toList() -> [Any?] { | ||||
|     return [ | ||||
|       id, | ||||
|       name, | ||||
|       type, | ||||
|       createdAt, | ||||
|       updatedAt, | ||||
|       durationInSeconds, | ||||
|     ] | ||||
|   } | ||||
|   static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool { | ||||
|     return deepEqualsMessages(lhs.toList(), rhs.toList())  } | ||||
|   func hash(into hasher: inout Hasher) { | ||||
|     deepHashMessages(value: toList(), hasher: &hasher) | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// Generated class from Pigeon that represents data sent in messages. | ||||
| struct PlatformAlbum: Hashable { | ||||
|   var id: String | ||||
|   var name: String | ||||
|   var updatedAt: Int64? = nil | ||||
|   var isCloud: Bool | ||||
|   var assetCount: Int64 | ||||
|  | ||||
|  | ||||
|   // swift-format-ignore: AlwaysUseLowerCamelCase | ||||
|   static func fromList(_ pigeonVar_list: [Any?]) -> PlatformAlbum? { | ||||
|     let id = pigeonVar_list[0] as! String | ||||
|     let name = pigeonVar_list[1] as! String | ||||
|     let updatedAt: Int64? = nilOrValue(pigeonVar_list[2]) | ||||
|     let isCloud = pigeonVar_list[3] as! Bool | ||||
|     let assetCount = pigeonVar_list[4] as! Int64 | ||||
|  | ||||
|     return PlatformAlbum( | ||||
|       id: id, | ||||
|       name: name, | ||||
|       updatedAt: updatedAt, | ||||
|       isCloud: isCloud, | ||||
|       assetCount: assetCount | ||||
|     ) | ||||
|   } | ||||
|   func toList() -> [Any?] { | ||||
|     return [ | ||||
|       id, | ||||
|       name, | ||||
|       updatedAt, | ||||
|       isCloud, | ||||
|       assetCount, | ||||
|     ] | ||||
|   } | ||||
|   static func == (lhs: PlatformAlbum, rhs: PlatformAlbum) -> Bool { | ||||
|     return deepEqualsMessages(lhs.toList(), rhs.toList())  } | ||||
|   func hash(into hasher: inout Hasher) { | ||||
|     deepHashMessages(value: toList(), hasher: &hasher) | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// Generated class from Pigeon that represents data sent in messages. | ||||
| struct SyncDelta: Hashable { | ||||
|   var hasChanges: Bool | ||||
|   var updates: [PlatformAsset] | ||||
|   var deletes: [String] | ||||
|   var assetAlbums: [String: [String]] | ||||
|  | ||||
|  | ||||
|   // swift-format-ignore: AlwaysUseLowerCamelCase | ||||
|   static func fromList(_ pigeonVar_list: [Any?]) -> SyncDelta? { | ||||
|     let hasChanges = pigeonVar_list[0] as! Bool | ||||
|     let updates = pigeonVar_list[1] as! [PlatformAsset] | ||||
|     let deletes = pigeonVar_list[2] as! [String] | ||||
|     let assetAlbums = pigeonVar_list[3] as! [String: [String]] | ||||
|  | ||||
|     return SyncDelta( | ||||
|       hasChanges: hasChanges, | ||||
|       updates: updates, | ||||
|       deletes: deletes, | ||||
|       assetAlbums: assetAlbums | ||||
|     ) | ||||
|   } | ||||
|   func toList() -> [Any?] { | ||||
|     return [ | ||||
|       hasChanges, | ||||
|       updates, | ||||
|       deletes, | ||||
|       assetAlbums, | ||||
|     ] | ||||
|   } | ||||
|   static func == (lhs: SyncDelta, rhs: SyncDelta) -> Bool { | ||||
|     return deepEqualsMessages(lhs.toList(), rhs.toList())  } | ||||
|   func hash(into hasher: inout Hasher) { | ||||
|     deepHashMessages(value: toList(), hasher: &hasher) | ||||
|   } | ||||
| } | ||||
|  | ||||
| private class MessagesPigeonCodecReader: FlutterStandardReader { | ||||
|   override func readValue(ofType type: UInt8) -> Any? { | ||||
|     switch type { | ||||
|     case 129: | ||||
|       return PlatformAsset.fromList(self.readValue() as! [Any?]) | ||||
|     case 130: | ||||
|       return PlatformAlbum.fromList(self.readValue() as! [Any?]) | ||||
|     case 131: | ||||
|       return SyncDelta.fromList(self.readValue() as! [Any?]) | ||||
|     default: | ||||
|       return super.readValue(ofType: type) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| private class MessagesPigeonCodecWriter: FlutterStandardWriter { | ||||
|   override func writeValue(_ value: Any) { | ||||
|     if let value = value as? PlatformAsset { | ||||
|       super.writeByte(129) | ||||
|       super.writeValue(value.toList()) | ||||
|     } else if let value = value as? PlatformAlbum { | ||||
|       super.writeByte(130) | ||||
|       super.writeValue(value.toList()) | ||||
|     } else if let value = value as? SyncDelta { | ||||
|       super.writeByte(131) | ||||
|       super.writeValue(value.toList()) | ||||
|     } else { | ||||
|       super.writeValue(value) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| private class MessagesPigeonCodecReaderWriter: FlutterStandardReaderWriter { | ||||
|   override func reader(with data: Data) -> FlutterStandardReader { | ||||
|     return MessagesPigeonCodecReader(data: data) | ||||
|   } | ||||
|  | ||||
|   override func writer(with data: NSMutableData) -> FlutterStandardWriter { | ||||
|     return MessagesPigeonCodecWriter(data: data) | ||||
|   } | ||||
| } | ||||
|  | ||||
| class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { | ||||
|   static let shared = MessagesPigeonCodec(readerWriter: MessagesPigeonCodecReaderWriter()) | ||||
| } | ||||
|  | ||||
| /// Generated protocol from Pigeon that represents a handler of messages from Flutter. | ||||
| protocol NativeSyncApi { | ||||
|   func shouldFullSync() throws -> Bool | ||||
|   func getMediaChanges() throws -> SyncDelta | ||||
|   func checkpointSync() throws | ||||
|   func clearSyncCheckpoint() throws | ||||
|   func getAssetIdsForAlbum(albumId: String) throws -> [String] | ||||
|   func getAlbums() throws -> [PlatformAlbum] | ||||
|   func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 | ||||
|   func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] | ||||
| } | ||||
|  | ||||
| /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. | ||||
| class NativeSyncApiSetup { | ||||
|   static var codec: FlutterStandardMessageCodec { MessagesPigeonCodec.shared } | ||||
|   /// Sets up an instance of `NativeSyncApi` to handle messages through the `binaryMessenger`. | ||||
|   static func setUp(binaryMessenger: FlutterBinaryMessenger, api: NativeSyncApi?, messageChannelSuffix: String = "") { | ||||
|     let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" | ||||
|     #if os(iOS) | ||||
|       let taskQueue = binaryMessenger.makeBackgroundTaskQueue?() | ||||
|     #else | ||||
|       let taskQueue: FlutterTaskQueue? = nil | ||||
|     #endif | ||||
|     let shouldFullSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | ||||
|     if let api = api { | ||||
|       shouldFullSyncChannel.setMessageHandler { _, reply in | ||||
|         do { | ||||
|           let result = try api.shouldFullSync() | ||||
|           reply(wrapResult(result)) | ||||
|         } catch { | ||||
|           reply(wrapError(error)) | ||||
|         } | ||||
|       } | ||||
|     } else { | ||||
|       shouldFullSyncChannel.setMessageHandler(nil) | ||||
|     } | ||||
|     let getMediaChangesChannel = taskQueue == nil | ||||
|       ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | ||||
|       : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) | ||||
|     if let api = api { | ||||
|       getMediaChangesChannel.setMessageHandler { _, reply in | ||||
|         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.NativeSyncApi.checkpointSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | ||||
|     if let api = api { | ||||
|       checkpointSyncChannel.setMessageHandler { _, reply in | ||||
|         do { | ||||
|           try api.checkpointSync() | ||||
|           reply(wrapResult(nil)) | ||||
|         } catch { | ||||
|           reply(wrapError(error)) | ||||
|         } | ||||
|       } | ||||
|     } else { | ||||
|       checkpointSyncChannel.setMessageHandler(nil) | ||||
|     } | ||||
|     let clearSyncCheckpointChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | ||||
|     if let api = api { | ||||
|       clearSyncCheckpointChannel.setMessageHandler { _, reply in | ||||
|         do { | ||||
|           try api.clearSyncCheckpoint() | ||||
|           reply(wrapResult(nil)) | ||||
|         } catch { | ||||
|           reply(wrapError(error)) | ||||
|         } | ||||
|       } | ||||
|     } else { | ||||
|       clearSyncCheckpointChannel.setMessageHandler(nil) | ||||
|     } | ||||
|     let getAssetIdsForAlbumChannel = taskQueue == nil | ||||
|       ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | ||||
|       : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) | ||||
|     if let api = api { | ||||
|       getAssetIdsForAlbumChannel.setMessageHandler { message, reply in | ||||
|         let args = message as! [Any?] | ||||
|         let albumIdArg = args[0] as! String | ||||
|         do { | ||||
|           let result = try api.getAssetIdsForAlbum(albumId: albumIdArg) | ||||
|           reply(wrapResult(result)) | ||||
|         } catch { | ||||
|           reply(wrapError(error)) | ||||
|         } | ||||
|       } | ||||
|     } else { | ||||
|       getAssetIdsForAlbumChannel.setMessageHandler(nil) | ||||
|     } | ||||
|     let getAlbumsChannel = taskQueue == nil | ||||
|       ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | ||||
|       : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) | ||||
|     if let api = api { | ||||
|       getAlbumsChannel.setMessageHandler { _, reply in | ||||
|         do { | ||||
|           let result = try api.getAlbums() | ||||
|           reply(wrapResult(result)) | ||||
|         } catch { | ||||
|           reply(wrapError(error)) | ||||
|         } | ||||
|       } | ||||
|     } else { | ||||
|       getAlbumsChannel.setMessageHandler(nil) | ||||
|     } | ||||
|     let getAssetsCountSinceChannel = taskQueue == nil | ||||
|       ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | ||||
|       : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) | ||||
|     if let api = api { | ||||
|       getAssetsCountSinceChannel.setMessageHandler { message, reply in | ||||
|         let args = message as! [Any?] | ||||
|         let albumIdArg = args[0] as! String | ||||
|         let timestampArg = args[1] as! Int64 | ||||
|         do { | ||||
|           let result = try api.getAssetsCountSince(albumId: albumIdArg, timestamp: timestampArg) | ||||
|           reply(wrapResult(result)) | ||||
|         } catch { | ||||
|           reply(wrapError(error)) | ||||
|         } | ||||
|       } | ||||
|     } else { | ||||
|       getAssetsCountSinceChannel.setMessageHandler(nil) | ||||
|     } | ||||
|     let getAssetsForAlbumChannel = taskQueue == nil | ||||
|       ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | ||||
|       : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) | ||||
|     if let api = api { | ||||
|       getAssetsForAlbumChannel.setMessageHandler { message, reply in | ||||
|         let args = message as! [Any?] | ||||
|         let albumIdArg = args[0] as! String | ||||
|         let updatedTimeCondArg: Int64? = nilOrValue(args[1]) | ||||
|         do { | ||||
|           let result = try api.getAssetsForAlbum(albumId: albumIdArg, updatedTimeCond: updatedTimeCondArg) | ||||
|           reply(wrapResult(result)) | ||||
|         } catch { | ||||
|           reply(wrapError(error)) | ||||
|         } | ||||
|       } | ||||
|     } else { | ||||
|       getAssetsForAlbumChannel.setMessageHandler(nil) | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										246
									
								
								mobile/ios/Runner/Sync/MessagesImpl.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										246
									
								
								mobile/ios/Runner/Sync/MessagesImpl.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,246 @@ | ||||
| import Photos | ||||
|  | ||||
| struct AssetWrapper: Hashable, Equatable { | ||||
|   let asset: PlatformAsset | ||||
|    | ||||
|   init(with asset: PlatformAsset) { | ||||
|     self.asset = asset | ||||
|   } | ||||
|    | ||||
|   func hash(into hasher: inout Hasher) { | ||||
|     hasher.combine(self.asset.id) | ||||
|   } | ||||
|    | ||||
|   static func == (lhs: AssetWrapper, rhs: AssetWrapper) -> Bool { | ||||
|     return lhs.asset.id == rhs.asset.id | ||||
|   } | ||||
| } | ||||
|  | ||||
| extension PHAsset { | ||||
|   func toPlatformAsset() -> PlatformAsset { | ||||
|     return PlatformAsset( | ||||
|       id: localIdentifier, | ||||
|       name: title(), | ||||
|       type: Int64(mediaType.rawValue), | ||||
|       createdAt: creationDate.map { Int64($0.timeIntervalSince1970) }, | ||||
|       updatedAt: modificationDate.map { Int64($0.timeIntervalSince1970) }, | ||||
|       durationInSeconds: Int64(duration) | ||||
|     ) | ||||
|   } | ||||
| } | ||||
|  | ||||
| class NativeSyncApiImpl: NativeSyncApi { | ||||
|   private let defaults: UserDefaults | ||||
|   private let changeTokenKey = "immich:changeToken" | ||||
|   private let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum] | ||||
|    | ||||
|   init(with defaults: UserDefaults = .standard) { | ||||
|     self.defaults = defaults | ||||
|   } | ||||
|    | ||||
|   @available(iOS 16, *) | ||||
|   private func getChangeToken() -> PHPersistentChangeToken? { | ||||
|     guard let data = defaults.data(forKey: changeTokenKey) else { | ||||
|       return nil | ||||
|     } | ||||
|     return try? NSKeyedUnarchiver.unarchivedObject(ofClass: PHPersistentChangeToken.self, from: data) | ||||
|   } | ||||
|    | ||||
|   @available(iOS 16, *) | ||||
|   private func saveChangeToken(token: PHPersistentChangeToken) -> Void { | ||||
|     guard let data = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true) else { | ||||
|       return | ||||
|     } | ||||
|     defaults.set(data, forKey: changeTokenKey) | ||||
|   } | ||||
|    | ||||
|   func clearSyncCheckpoint() -> Void { | ||||
|     defaults.removeObject(forKey: changeTokenKey) | ||||
|   } | ||||
|    | ||||
|   func checkpointSync() { | ||||
|     guard #available(iOS 16, *) else { | ||||
|       return | ||||
|     } | ||||
|     saveChangeToken(token: PHPhotoLibrary.shared().currentChangeToken) | ||||
|   } | ||||
|    | ||||
|   func shouldFullSync() -> Bool { | ||||
|     guard #available(iOS 16, *), | ||||
|           PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized, | ||||
|           let storedToken = getChangeToken() else { | ||||
|       // When we do not have access to photo library, older iOS version or No token available, fallback to full sync | ||||
|       return true | ||||
|     } | ||||
|      | ||||
|     guard let _ = try? PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken) else { | ||||
|       // Cannot fetch persistent changes | ||||
|       return true | ||||
|     } | ||||
|      | ||||
|     return false | ||||
|   } | ||||
|    | ||||
|   func getAlbums() throws -> [PlatformAlbum] { | ||||
|     var albums: [PlatformAlbum] = [] | ||||
|      | ||||
|     albumTypes.forEach { type in | ||||
|       let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil) | ||||
|       collections.enumerateObjects { (album, _, _) in | ||||
|         let options = PHFetchOptions() | ||||
|         options.sortDescriptors = [NSSortDescriptor(key: "modificationDate", ascending: false)] | ||||
|         let assets = PHAsset.fetchAssets(in: album, options: options) | ||||
|         let isCloud = album.assetCollectionSubtype == .albumCloudShared || album.assetCollectionSubtype == .albumMyPhotoStream | ||||
|          | ||||
|         var domainAlbum = PlatformAlbum( | ||||
|           id: album.localIdentifier, | ||||
|           name: album.localizedTitle!, | ||||
|           updatedAt: nil, | ||||
|           isCloud: isCloud, | ||||
|           assetCount: Int64(assets.count) | ||||
|         ) | ||||
|          | ||||
|         if let firstAsset = assets.firstObject { | ||||
|           domainAlbum.updatedAt = firstAsset.modificationDate.map { Int64($0.timeIntervalSince1970) } | ||||
|         } | ||||
|          | ||||
|         albums.append(domainAlbum) | ||||
|       } | ||||
|     } | ||||
|     return albums.sorted { $0.id < $1.id } | ||||
|   } | ||||
|    | ||||
|   func getMediaChanges() throws -> SyncDelta { | ||||
|     guard #available(iOS 16, *) else { | ||||
|       throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature requires iOS 16 or later.", details: nil) | ||||
|     } | ||||
|      | ||||
|     guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else { | ||||
|       throw PigeonError(code: "NO_AUTH", message: "No photo library access", details: nil) | ||||
|     } | ||||
|      | ||||
|     guard let storedToken = getChangeToken() else { | ||||
|       // No token exists, definitely need a full sync | ||||
|       print("MediaManager::getMediaChanges: No token found") | ||||
|       throw PigeonError(code: "NO_TOKEN", message: "No stored change token", details: nil) | ||||
|     } | ||||
|      | ||||
|     let currentToken = PHPhotoLibrary.shared().currentChangeToken | ||||
|     if storedToken == currentToken { | ||||
|       return SyncDelta(hasChanges: false, updates: [], deletes: [], assetAlbums: [:]) | ||||
|     } | ||||
|      | ||||
|     do { | ||||
|       let changes = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken) | ||||
|        | ||||
|       var updatedAssets: Set<AssetWrapper> = [] | ||||
|       var deletedAssets: Set<String> = [] | ||||
|        | ||||
|       for change in changes { | ||||
|         guard let details = try? change.changeDetails(for: PHObjectType.asset) else { continue } | ||||
|          | ||||
|         let updated = details.updatedLocalIdentifiers.union(details.insertedLocalIdentifiers) | ||||
|         deletedAssets.formUnion(details.deletedLocalIdentifiers) | ||||
|          | ||||
|         if (updated.isEmpty) { continue } | ||||
|          | ||||
|         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 = PlatformAsset( | ||||
|             id: asset.localIdentifier, | ||||
|             name: "", | ||||
|             type: 0, | ||||
|             createdAt: nil, | ||||
|             updatedAt: nil, | ||||
|             durationInSeconds: 0 | ||||
|           ) | ||||
|           if (updatedAssets.contains(AssetWrapper(with: predicate))) { | ||||
|             continue | ||||
|           } | ||||
|            | ||||
|           let domainAsset = AssetWrapper(with: asset.toPlatformAsset()) | ||||
|           updatedAssets.insert(domainAsset) | ||||
|         } | ||||
|       } | ||||
|        | ||||
|       let updates = Array(updatedAssets.map { $0.asset }) | ||||
|       return SyncDelta(hasChanges: true, updates: updates, deletes: Array(deletedAssets), assetAlbums: buildAssetAlbumsMap(assets: updates)) | ||||
|     } | ||||
|   } | ||||
|    | ||||
|    | ||||
|   private func buildAssetAlbumsMap(assets: Array<PlatformAsset>) -> [String: [String]] { | ||||
|     guard !assets.isEmpty else { | ||||
|       return [:] | ||||
|     } | ||||
|      | ||||
|     var albumAssets: [String: [String]] = [:] | ||||
|      | ||||
|     for type in albumTypes { | ||||
|       let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil) | ||||
|       collections.enumerateObjects { (album, _, _) in | ||||
|         let options = PHFetchOptions() | ||||
|         options.predicate = NSPredicate(format: "localIdentifier IN %@", assets.map(\.id)) | ||||
|         let result = PHAsset.fetchAssets(in: album, options: options) | ||||
|         result.enumerateObjects { (asset, _, _) in | ||||
|           albumAssets[asset.localIdentifier, default: []].append(album.localIdentifier) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return albumAssets | ||||
|   } | ||||
|    | ||||
|   func getAssetIdsForAlbum(albumId: String) throws -> [String] { | ||||
|     let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil) | ||||
|     guard let album = collections.firstObject else { | ||||
|       return [] | ||||
|     } | ||||
|      | ||||
|     var ids: [String] = [] | ||||
|     let assets = PHAsset.fetchAssets(in: album, options: nil) | ||||
|     assets.enumerateObjects { (asset, _, _) in | ||||
|       ids.append(asset.localIdentifier) | ||||
|     } | ||||
|     return ids | ||||
|   } | ||||
|    | ||||
|   func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 { | ||||
|     let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil) | ||||
|     guard let album = collections.firstObject else { | ||||
|       return 0 | ||||
|     } | ||||
|      | ||||
|     let date = NSDate(timeIntervalSince1970: TimeInterval(timestamp)) | ||||
|     let options = PHFetchOptions() | ||||
|     options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date) | ||||
|     let assets = PHAsset.fetchAssets(in: album, options: options) | ||||
|     return Int64(assets.count) | ||||
|   } | ||||
|    | ||||
|   func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] { | ||||
|     let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil) | ||||
|     guard let album = collections.firstObject else { | ||||
|       return [] | ||||
|     } | ||||
|      | ||||
|     let options = PHFetchOptions() | ||||
|     if(updatedTimeCond != nil) { | ||||
|       let date = NSDate(timeIntervalSince1970: TimeInterval(updatedTimeCond!)) | ||||
|       options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date) | ||||
|     } | ||||
|  | ||||
|     let result = PHAsset.fetchAssets(in: album, options: options) | ||||
|     if(result.count == 0) { | ||||
|       return [] | ||||
|     } | ||||
|      | ||||
|     var assets: [PlatformAsset] = [] | ||||
|     result.enumerateObjects { (asset, _, _) in | ||||
|       assets.append(asset.toPlatformAsset()) | ||||
|     } | ||||
|     return assets | ||||
|   } | ||||
| } | ||||
| @@ -7,6 +7,7 @@ const int kLogTruncateLimit = 250; | ||||
|  | ||||
| // Sync | ||||
| const int kSyncEventBatchSize = 5000; | ||||
| const int kFetchLocalAssetsBatchSize = 40000; | ||||
|  | ||||
| // Hash batch limits | ||||
| const int kBatchHashFileLimit = 128; | ||||
|   | ||||
							
								
								
									
										34
									
								
								mobile/lib/domain/interfaces/local_album.interface.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								mobile/lib/domain/interfaces/local_album.interface.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| import 'package:immich_mobile/domain/interfaces/db.interface.dart'; | ||||
| import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; | ||||
| import 'package:immich_mobile/domain/models/local_album.model.dart'; | ||||
|  | ||||
| abstract interface class ILocalAlbumRepository implements IDatabaseRepository { | ||||
|   Future<List<LocalAlbum>> getAll({SortLocalAlbumsBy? sortBy}); | ||||
|  | ||||
|   Future<List<LocalAsset>> getAssetsForAlbum(String albumId); | ||||
|  | ||||
|   Future<List<String>> getAssetIdsForAlbum(String albumId); | ||||
|  | ||||
|   Future<void> upsert( | ||||
|     LocalAlbum album, { | ||||
|     Iterable<LocalAsset> toUpsert = const [], | ||||
|     Iterable<String> toDelete = const [], | ||||
|   }); | ||||
|  | ||||
|   Future<void> updateAll(Iterable<LocalAlbum> albums); | ||||
|  | ||||
|   Future<void> delete(String albumId); | ||||
|  | ||||
|   Future<void> processDelta({ | ||||
|     required List<LocalAsset> updates, | ||||
|     required List<String> deletes, | ||||
|     required Map<String, List<String>> assetAlbums, | ||||
|   }); | ||||
|  | ||||
|   Future<void> syncAlbumDeletes( | ||||
|     String albumId, | ||||
|     Iterable<String> assetIdsToKeep, | ||||
|   ); | ||||
| } | ||||
|  | ||||
| enum SortLocalAlbumsBy { id } | ||||
							
								
								
									
										47
									
								
								mobile/lib/domain/models/asset/asset.model.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								mobile/lib/domain/models/asset/asset.model.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| part of 'base_asset.model.dart'; | ||||
|  | ||||
| // Model for an asset stored in the server | ||||
| class Asset extends BaseAsset { | ||||
|   final String id; | ||||
|   final String? localId; | ||||
|  | ||||
|   const Asset({ | ||||
|     required this.id, | ||||
|     this.localId, | ||||
|     required super.name, | ||||
|     required super.checksum, | ||||
|     required super.type, | ||||
|     required super.createdAt, | ||||
|     required super.updatedAt, | ||||
|     super.width, | ||||
|     super.height, | ||||
|     super.durationInSeconds, | ||||
|     super.isFavorite = false, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return '''Asset { | ||||
|    id: $id, | ||||
|    name: $name, | ||||
|    type: $type, | ||||
|    createdAt: $createdAt, | ||||
|    updatedAt: $updatedAt, | ||||
|    width: ${width ?? "<NA>"}, | ||||
|    height: ${height ?? "<NA>"}, | ||||
|    durationInSeconds: ${durationInSeconds ?? "<NA>"}, | ||||
|    localId: ${localId ?? "<NA>"}, | ||||
|    isFavorite: $isFavorite, | ||||
|  }'''; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     if (other is! Asset) return false; | ||||
|     if (identical(this, other)) return true; | ||||
|     return super == other && id == other.id && localId == other.localId; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode => super.hashCode ^ id.hashCode ^ localId.hashCode; | ||||
| } | ||||
							
								
								
									
										76
									
								
								mobile/lib/domain/models/asset/base_asset.model.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								mobile/lib/domain/models/asset/base_asset.model.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| part 'asset.model.dart'; | ||||
| part 'local_asset.model.dart'; | ||||
|  | ||||
| enum AssetType { | ||||
|   // do not change this order! | ||||
|   other, | ||||
|   image, | ||||
|   video, | ||||
|   audio, | ||||
| } | ||||
|  | ||||
| sealed class BaseAsset { | ||||
|   final String name; | ||||
|   final String? checksum; | ||||
|   final AssetType type; | ||||
|   final DateTime createdAt; | ||||
|   final DateTime updatedAt; | ||||
|   final int? width; | ||||
|   final int? height; | ||||
|   final int? durationInSeconds; | ||||
|   final bool isFavorite; | ||||
|  | ||||
|   const BaseAsset({ | ||||
|     required this.name, | ||||
|     required this.checksum, | ||||
|     required this.type, | ||||
|     required this.createdAt, | ||||
|     required this.updatedAt, | ||||
|     this.width, | ||||
|     this.height, | ||||
|     this.durationInSeconds, | ||||
|     this.isFavorite = false, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return '''BaseAsset { | ||||
|   name: $name, | ||||
|   type: $type, | ||||
|   createdAt: $createdAt, | ||||
|   updatedAt: $updatedAt, | ||||
|   width: ${width ?? "<NA>"}, | ||||
|   height: ${height ?? "<NA>"}, | ||||
|   durationInSeconds: ${durationInSeconds ?? "<NA>"}, | ||||
|   isFavorite: $isFavorite, | ||||
| }'''; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     if (identical(this, other)) return true; | ||||
|     if (other is BaseAsset) { | ||||
|       return name == other.name && | ||||
|           type == other.type && | ||||
|           createdAt == other.createdAt && | ||||
|           updatedAt == other.updatedAt && | ||||
|           width == other.width && | ||||
|           height == other.height && | ||||
|           durationInSeconds == other.durationInSeconds && | ||||
|           isFavorite == other.isFavorite; | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     return name.hashCode ^ | ||||
|         type.hashCode ^ | ||||
|         createdAt.hashCode ^ | ||||
|         updatedAt.hashCode ^ | ||||
|         width.hashCode ^ | ||||
|         height.hashCode ^ | ||||
|         durationInSeconds.hashCode ^ | ||||
|         isFavorite.hashCode; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										74
									
								
								mobile/lib/domain/models/asset/local_asset.model.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								mobile/lib/domain/models/asset/local_asset.model.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| part of 'base_asset.model.dart'; | ||||
|  | ||||
| class LocalAsset extends BaseAsset { | ||||
|   final String id; | ||||
|   final String? remoteId; | ||||
|  | ||||
|   const LocalAsset({ | ||||
|     required this.id, | ||||
|     this.remoteId, | ||||
|     required super.name, | ||||
|     super.checksum, | ||||
|     required super.type, | ||||
|     required super.createdAt, | ||||
|     required super.updatedAt, | ||||
|     super.width, | ||||
|     super.height, | ||||
|     super.durationInSeconds, | ||||
|     super.isFavorite = false, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return '''LocalAsset { | ||||
|    id: $id, | ||||
|    name: $name, | ||||
|    type: $type, | ||||
|    createdAt: $createdAt, | ||||
|    updatedAt: $updatedAt, | ||||
|    width: ${width ?? "<NA>"}, | ||||
|    height: ${height ?? "<NA>"}, | ||||
|    durationInSeconds: ${durationInSeconds ?? "<NA>"}, | ||||
|    remoteId: ${remoteId ?? "<NA>"} | ||||
|    isFavorite: $isFavorite, | ||||
|  }'''; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     if (other is! LocalAsset) return false; | ||||
|     if (identical(this, other)) return true; | ||||
|     return super == other && id == other.id && remoteId == other.remoteId; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode => super.hashCode ^ id.hashCode ^ remoteId.hashCode; | ||||
|  | ||||
|   LocalAsset copyWith({ | ||||
|     String? id, | ||||
|     String? remoteId, | ||||
|     String? name, | ||||
|     String? checksum, | ||||
|     AssetType? type, | ||||
|     DateTime? createdAt, | ||||
|     DateTime? updatedAt, | ||||
|     int? width, | ||||
|     int? height, | ||||
|     int? durationInSeconds, | ||||
|     bool? isFavorite, | ||||
|   }) { | ||||
|     return LocalAsset( | ||||
|       id: id ?? this.id, | ||||
|       remoteId: remoteId ?? this.remoteId, | ||||
|       name: name ?? this.name, | ||||
|       checksum: checksum ?? this.checksum, | ||||
|       type: type ?? this.type, | ||||
|       createdAt: createdAt ?? this.createdAt, | ||||
|       updatedAt: updatedAt ?? this.updatedAt, | ||||
|       width: width ?? this.width, | ||||
|       height: height ?? this.height, | ||||
|       durationInSeconds: durationInSeconds ?? this.durationInSeconds, | ||||
|       isFavorite: isFavorite ?? this.isFavorite, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										70
									
								
								mobile/lib/domain/models/local_album.model.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								mobile/lib/domain/models/local_album.model.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| enum BackupSelection { | ||||
|   none, | ||||
|   selected, | ||||
|   excluded, | ||||
| } | ||||
|  | ||||
| class LocalAlbum { | ||||
|   final String id; | ||||
|   final String name; | ||||
|   final DateTime updatedAt; | ||||
|  | ||||
|   final int assetCount; | ||||
|   final BackupSelection backupSelection; | ||||
|  | ||||
|   const LocalAlbum({ | ||||
|     required this.id, | ||||
|     required this.name, | ||||
|     required this.updatedAt, | ||||
|     this.assetCount = 0, | ||||
|     this.backupSelection = BackupSelection.none, | ||||
|   }); | ||||
|  | ||||
|   LocalAlbum copyWith({ | ||||
|     String? id, | ||||
|     String? name, | ||||
|     DateTime? updatedAt, | ||||
|     int? assetCount, | ||||
|     BackupSelection? backupSelection, | ||||
|   }) { | ||||
|     return LocalAlbum( | ||||
|       id: id ?? this.id, | ||||
|       name: name ?? this.name, | ||||
|       updatedAt: updatedAt ?? this.updatedAt, | ||||
|       assetCount: assetCount ?? this.assetCount, | ||||
|       backupSelection: backupSelection ?? this.backupSelection, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   bool operator ==(Object other) { | ||||
|     if (other is! LocalAlbum) return false; | ||||
|     if (identical(this, other)) return true; | ||||
|  | ||||
|     return other.id == id && | ||||
|         other.name == name && | ||||
|         other.updatedAt == updatedAt && | ||||
|         other.assetCount == assetCount && | ||||
|         other.backupSelection == backupSelection; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   int get hashCode { | ||||
|     return id.hashCode ^ | ||||
|         name.hashCode ^ | ||||
|         updatedAt.hashCode ^ | ||||
|         assetCount.hashCode ^ | ||||
|         backupSelection.hashCode; | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     return '''LocalAlbum: { | ||||
| id: $id, | ||||
| name: $name, | ||||
| updatedAt: $updatedAt, | ||||
| assetCount: $assetCount, | ||||
| backupSelection: $backupSelection, | ||||
| }'''; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										379
									
								
								mobile/lib/domain/services/local_sync.service.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										379
									
								
								mobile/lib/domain/services/local_sync.service.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,379 @@ | ||||
| import 'dart:async'; | ||||
|  | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:flutter/widgets.dart'; | ||||
| import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; | ||||
| import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; | ||||
| import 'package:immich_mobile/domain/models/local_album.model.dart'; | ||||
| import 'package:immich_mobile/domain/models/store.model.dart'; | ||||
| import 'package:immich_mobile/domain/services/store.service.dart'; | ||||
| import 'package:immich_mobile/platform/native_sync_api.g.dart'; | ||||
| import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart'; | ||||
| import 'package:immich_mobile/utils/diff.dart'; | ||||
| import 'package:logging/logging.dart'; | ||||
| import 'package:platform/platform.dart'; | ||||
|  | ||||
| class LocalSyncService { | ||||
|   final ILocalAlbumRepository _localAlbumRepository; | ||||
|   final NativeSyncApi _nativeSyncApi; | ||||
|   final Platform _platform; | ||||
|   final StoreService _storeService; | ||||
|   final Logger _log = Logger("DeviceSyncService"); | ||||
|  | ||||
|   LocalSyncService({ | ||||
|     required ILocalAlbumRepository localAlbumRepository, | ||||
|     required NativeSyncApi nativeSyncApi, | ||||
|     required StoreService storeService, | ||||
|     Platform? platform, | ||||
|   })  : _localAlbumRepository = localAlbumRepository, | ||||
|         _nativeSyncApi = nativeSyncApi, | ||||
|         _storeService = storeService, | ||||
|         _platform = platform ?? const LocalPlatform(); | ||||
|  | ||||
|   bool get _ignoreIcloudAssets => | ||||
|       _storeService.get(StoreKey.ignoreIcloudAssets, false) == true; | ||||
|  | ||||
|   Future<void> sync({bool full = false}) async { | ||||
|     final Stopwatch stopwatch = Stopwatch()..start(); | ||||
|     try { | ||||
|       if (full || await _nativeSyncApi.shouldFullSync()) { | ||||
|         _log.fine("Full sync request from ${full ? "user" : "native"}"); | ||||
|         DLog.log("Full sync request from ${full ? "user" : "native"}"); | ||||
|         return await fullSync(); | ||||
|       } | ||||
|  | ||||
|       final delta = await _nativeSyncApi.getMediaChanges(); | ||||
|       if (!delta.hasChanges) { | ||||
|         _log.fine("No media changes detected. Skipping sync"); | ||||
|         DLog.log("No media changes detected. Skipping sync"); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       DLog.log("Delta updated: ${delta.updates.length}"); | ||||
|       DLog.log("Delta deleted: ${delta.deletes.length}"); | ||||
|  | ||||
|       final deviceAlbums = await _nativeSyncApi.getAlbums(); | ||||
|       await _localAlbumRepository.updateAll(deviceAlbums.toLocalAlbums()); | ||||
|       await _localAlbumRepository.processDelta( | ||||
|         updates: delta.updates.toLocalAssets(), | ||||
|         deletes: delta.deletes, | ||||
|         assetAlbums: delta.assetAlbums, | ||||
|       ); | ||||
|  | ||||
|       final dbAlbums = await _localAlbumRepository.getAll(); | ||||
|       // On Android, we need to sync all albums since it is not possible to | ||||
|       // detect album deletions from the native side | ||||
|       if (_platform.isAndroid) { | ||||
|         for (final album in dbAlbums) { | ||||
|           final deviceIds = await _nativeSyncApi.getAssetIdsForAlbum(album.id); | ||||
|           await _localAlbumRepository.syncAlbumDeletes(album.id, deviceIds); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (_platform.isIOS) { | ||||
|         // On iOS, we need to full sync albums that are marked as cloud as the delta sync | ||||
|         // does not include changes for cloud albums. If ignoreIcloudAssets is enabled, | ||||
|         // remove the albums from the local database from the previous sync | ||||
|         final cloudAlbums = | ||||
|             deviceAlbums.where((a) => a.isCloud).toLocalAlbums(); | ||||
|         for (final album in cloudAlbums) { | ||||
|           final dbAlbum = dbAlbums.firstWhereOrNull((a) => a.id == album.id); | ||||
|           if (dbAlbum == null) { | ||||
|             _log.warning( | ||||
|               "Cloud album ${album.name} not found in local database. Skipping sync.", | ||||
|             ); | ||||
|             continue; | ||||
|           } | ||||
|           if (_ignoreIcloudAssets) { | ||||
|             await removeAlbum(dbAlbum); | ||||
|           } else { | ||||
|             await updateAlbum(dbAlbum, album); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       await _nativeSyncApi.checkpointSync(); | ||||
|     } catch (e, s) { | ||||
|       _log.severe("Error performing device sync", e, s); | ||||
|     } finally { | ||||
|       stopwatch.stop(); | ||||
|       _log.info("Device sync took - ${stopwatch.elapsedMilliseconds}ms"); | ||||
|       DLog.log("Device sync took - ${stopwatch.elapsedMilliseconds}ms"); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> fullSync() async { | ||||
|     try { | ||||
|       final Stopwatch stopwatch = Stopwatch()..start(); | ||||
|  | ||||
|       List<PlatformAlbum> deviceAlbums = | ||||
|           List.of(await _nativeSyncApi.getAlbums()); | ||||
|       if (_platform.isIOS && _ignoreIcloudAssets) { | ||||
|         deviceAlbums.removeWhere((album) => album.isCloud); | ||||
|       } | ||||
|  | ||||
|       final dbAlbums = | ||||
|           await _localAlbumRepository.getAll(sortBy: SortLocalAlbumsBy.id); | ||||
|  | ||||
|       await diffSortedLists( | ||||
|         dbAlbums, | ||||
|         deviceAlbums.toLocalAlbums(), | ||||
|         compare: (a, b) => a.id.compareTo(b.id), | ||||
|         both: updateAlbum, | ||||
|         onlyFirst: removeAlbum, | ||||
|         onlySecond: addAlbum, | ||||
|       ); | ||||
|  | ||||
|       await _nativeSyncApi.checkpointSync(); | ||||
|       stopwatch.stop(); | ||||
|       _log.info("Full device sync took - ${stopwatch.elapsedMilliseconds}ms"); | ||||
|       DLog.log("Full device sync took - ${stopwatch.elapsedMilliseconds}ms"); | ||||
|     } catch (e, s) { | ||||
|       _log.severe("Error performing full device sync", e, s); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> addAlbum(LocalAlbum album) async { | ||||
|     try { | ||||
|       _log.fine("Adding device album ${album.name}"); | ||||
|  | ||||
|       final assets = album.assetCount > 0 | ||||
|           ? await _nativeSyncApi.getAssetsForAlbum(album.id) | ||||
|           : <PlatformAsset>[]; | ||||
|  | ||||
|       await _localAlbumRepository.upsert( | ||||
|         album, | ||||
|         toUpsert: assets.toLocalAssets(), | ||||
|       ); | ||||
|       _log.fine("Successfully added device album ${album.name}"); | ||||
|     } catch (e, s) { | ||||
|       _log.warning("Error while adding device album", e, s); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> removeAlbum(LocalAlbum a) async { | ||||
|     _log.fine("Removing device album ${a.name}"); | ||||
|     try { | ||||
|       // Asset deletion is handled in the repository | ||||
|       await _localAlbumRepository.delete(a.id); | ||||
|     } catch (e, s) { | ||||
|       _log.warning("Error while removing device album", e, s); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // The deviceAlbum is ignored since we are going to refresh it anyways | ||||
|   FutureOr<bool> updateAlbum(LocalAlbum dbAlbum, LocalAlbum deviceAlbum) async { | ||||
|     try { | ||||
|       _log.fine("Syncing device album ${dbAlbum.name}"); | ||||
|  | ||||
|       if (_albumsEqual(deviceAlbum, dbAlbum)) { | ||||
|         _log.fine( | ||||
|           "Device album ${dbAlbum.name} has not changed. Skipping sync.", | ||||
|         ); | ||||
|         return false; | ||||
|       } | ||||
|  | ||||
|       _log.fine("Device album ${dbAlbum.name} has changed. Syncing..."); | ||||
|  | ||||
|       // Faster path - only new assets added | ||||
|       if (await checkAddition(dbAlbum, deviceAlbum)) { | ||||
|         _log.fine("Fast synced device album ${dbAlbum.name}"); | ||||
|         DLog.log("Fast synced device album ${dbAlbum.name}"); | ||||
|         return true; | ||||
|       } | ||||
|  | ||||
|       // Slower path - full sync | ||||
|       return await fullDiff(dbAlbum, deviceAlbum); | ||||
|     } catch (e, s) { | ||||
|       _log.warning("Error while diff device album", e, s); | ||||
|     } | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   @visibleForTesting | ||||
|   // The [deviceAlbum] is expected to be refreshed before calling this method | ||||
|   // with modified time and asset count | ||||
|   Future<bool> checkAddition( | ||||
|     LocalAlbum dbAlbum, | ||||
|     LocalAlbum deviceAlbum, | ||||
|   ) async { | ||||
|     try { | ||||
|       _log.fine("Fast syncing device album ${dbAlbum.name}"); | ||||
|       // Assets has been modified | ||||
|       if (deviceAlbum.assetCount <= dbAlbum.assetCount) { | ||||
|         _log.fine("Local album has modifications. Proceeding to full sync"); | ||||
|         return false; | ||||
|       } | ||||
|  | ||||
|       final updatedTime = | ||||
|           (dbAlbum.updatedAt.millisecondsSinceEpoch ~/ 1000) + 1; | ||||
|       final newAssetsCount = | ||||
|           await _nativeSyncApi.getAssetsCountSince(deviceAlbum.id, updatedTime); | ||||
|  | ||||
|       // Early return if no new assets were found | ||||
|       if (newAssetsCount == 0) { | ||||
|         _log.fine( | ||||
|           "No new assets found despite album having changes. Proceeding to full sync for ${dbAlbum.name}", | ||||
|         ); | ||||
|         return false; | ||||
|       } | ||||
|  | ||||
|       // Check whether there is only addition or if there has been deletions | ||||
|       if (deviceAlbum.assetCount != dbAlbum.assetCount + newAssetsCount) { | ||||
|         _log.fine("Local album has modifications. Proceeding to full sync"); | ||||
|         return false; | ||||
|       } | ||||
|  | ||||
|       final newAssets = await _nativeSyncApi.getAssetsForAlbum( | ||||
|         deviceAlbum.id, | ||||
|         updatedTimeCond: updatedTime, | ||||
|       ); | ||||
|  | ||||
|       await _localAlbumRepository.upsert( | ||||
|         deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection), | ||||
|         toUpsert: newAssets.toLocalAssets(), | ||||
|       ); | ||||
|  | ||||
|       return true; | ||||
|     } catch (e, s) { | ||||
|       _log.warning("Error on fast syncing local album: ${dbAlbum.name}", e, s); | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   @visibleForTesting | ||||
|   // The [deviceAlbum] is expected to be refreshed before calling this method | ||||
|   // with modified time and asset count | ||||
|   Future<bool> fullDiff(LocalAlbum dbAlbum, LocalAlbum deviceAlbum) async { | ||||
|     try { | ||||
|       final assetsInDevice = deviceAlbum.assetCount > 0 | ||||
|           ? await _nativeSyncApi | ||||
|               .getAssetsForAlbum(deviceAlbum.id) | ||||
|               .then((a) => a.toLocalAssets()) | ||||
|           : <LocalAsset>[]; | ||||
|       final assetsInDb = dbAlbum.assetCount > 0 | ||||
|           ? await _localAlbumRepository.getAssetsForAlbum(dbAlbum.id) | ||||
|           : <LocalAsset>[]; | ||||
|  | ||||
|       if (deviceAlbum.assetCount == 0) { | ||||
|         _log.fine( | ||||
|           "Device album ${deviceAlbum.name} is empty. Removing assets from DB.", | ||||
|         ); | ||||
|         await _localAlbumRepository.upsert( | ||||
|           deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection), | ||||
|           toDelete: assetsInDb.map((a) => a.id), | ||||
|         ); | ||||
|         return true; | ||||
|       } | ||||
|  | ||||
|       final updatedDeviceAlbum = deviceAlbum.copyWith( | ||||
|         backupSelection: dbAlbum.backupSelection, | ||||
|       ); | ||||
|  | ||||
|       if (dbAlbum.assetCount == 0) { | ||||
|         _log.fine( | ||||
|           "Device album ${deviceAlbum.name} is empty. Adding assets to DB.", | ||||
|         ); | ||||
|         await _localAlbumRepository.upsert( | ||||
|           updatedDeviceAlbum, | ||||
|           toUpsert: assetsInDevice, | ||||
|         ); | ||||
|         return true; | ||||
|       } | ||||
|  | ||||
|       assert(assetsInDb.isSortedBy((a) => a.id)); | ||||
|       assetsInDevice.sort((a, b) => a.id.compareTo(b.id)); | ||||
|  | ||||
|       final assetsToUpsert = <LocalAsset>[]; | ||||
|       final assetsToDelete = <String>[]; | ||||
|  | ||||
|       diffSortedListsSync( | ||||
|         assetsInDb, | ||||
|         assetsInDevice, | ||||
|         compare: (a, b) => a.id.compareTo(b.id), | ||||
|         both: (dbAsset, deviceAsset) { | ||||
|           // Custom comparison to check if the asset has been modified without | ||||
|           // comparing the checksum | ||||
|           if (!_assetsEqual(dbAsset, deviceAsset)) { | ||||
|             assetsToUpsert.add(deviceAsset); | ||||
|             return true; | ||||
|           } | ||||
|           return false; | ||||
|         }, | ||||
|         onlyFirst: (dbAsset) => assetsToDelete.add(dbAsset.id), | ||||
|         onlySecond: (deviceAsset) => assetsToUpsert.add(deviceAsset), | ||||
|       ); | ||||
|  | ||||
|       _log.fine( | ||||
|         "Syncing ${deviceAlbum.name}. ${assetsToUpsert.length} assets to add/update and ${assetsToDelete.length} assets to delete", | ||||
|       ); | ||||
|  | ||||
|       if (assetsToUpsert.isEmpty && assetsToDelete.isEmpty) { | ||||
|         _log.fine( | ||||
|           "No asset changes detected in album ${deviceAlbum.name}. Updating metadata.", | ||||
|         ); | ||||
|         _localAlbumRepository.upsert(updatedDeviceAlbum); | ||||
|         return true; | ||||
|       } | ||||
|  | ||||
|       await _localAlbumRepository.upsert( | ||||
|         updatedDeviceAlbum, | ||||
|         toUpsert: assetsToUpsert, | ||||
|         toDelete: assetsToDelete, | ||||
|       ); | ||||
|  | ||||
|       return true; | ||||
|     } catch (e, s) { | ||||
|       _log.warning("Error on full syncing local album: ${dbAlbum.name}", e, s); | ||||
|     } | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   bool _assetsEqual(LocalAsset a, LocalAsset b) { | ||||
|     return a.updatedAt.isAtSameMomentAs(b.updatedAt) && | ||||
|         a.createdAt.isAtSameMomentAs(b.createdAt) && | ||||
|         a.width == b.width && | ||||
|         a.height == b.height && | ||||
|         a.durationInSeconds == b.durationInSeconds; | ||||
|   } | ||||
|  | ||||
|   bool _albumsEqual(LocalAlbum a, LocalAlbum b) { | ||||
|     return a.name == b.name && | ||||
|         a.assetCount == b.assetCount && | ||||
|         a.updatedAt.isAtSameMomentAs(b.updatedAt); | ||||
|   } | ||||
| } | ||||
|  | ||||
| extension on Iterable<PlatformAlbum> { | ||||
|   List<LocalAlbum> toLocalAlbums() { | ||||
|     return map( | ||||
|       (e) => LocalAlbum( | ||||
|         id: e.id, | ||||
|         name: e.name, | ||||
|         updatedAt: e.updatedAt == null | ||||
|             ? DateTime.now() | ||||
|             : DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000), | ||||
|         assetCount: e.assetCount, | ||||
|       ), | ||||
|     ).toList(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| extension on Iterable<PlatformAsset> { | ||||
|   List<LocalAsset> toLocalAssets() { | ||||
|     return map( | ||||
|       (e) => LocalAsset( | ||||
|         id: e.id, | ||||
|         name: e.name, | ||||
|         type: AssetType.values.elementAtOrNull(e.type) ?? AssetType.other, | ||||
|         createdAt: e.createdAt == null | ||||
|             ? DateTime.now() | ||||
|             : DateTime.fromMillisecondsSinceEpoch(e.createdAt! * 1000), | ||||
|         updatedAt: e.updatedAt == null | ||||
|             ? DateTime.now() | ||||
|             : DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000), | ||||
|         durationInSeconds: e.durationInSeconds, | ||||
|       ), | ||||
|     ).toList(); | ||||
|   } | ||||
| } | ||||
| @@ -1,13 +1,12 @@ | ||||
| // ignore_for_file: avoid-passing-async-when-sync-expected | ||||
|  | ||||
| import 'dart:async'; | ||||
|  | ||||
| import 'package:immich_mobile/providers/infrastructure/sync_stream.provider.dart'; | ||||
| import 'package:immich_mobile/providers/infrastructure/sync.provider.dart'; | ||||
| import 'package:immich_mobile/utils/isolate.dart'; | ||||
| import 'package:worker_manager/worker_manager.dart'; | ||||
|  | ||||
| class BackgroundSyncManager { | ||||
|   Cancelable<void>? _syncTask; | ||||
|   Cancelable<void>? _deviceAlbumSyncTask; | ||||
|  | ||||
|   BackgroundSyncManager(); | ||||
|  | ||||
| @@ -23,7 +22,30 @@ class BackgroundSyncManager { | ||||
|     return Future.wait(futures); | ||||
|   } | ||||
|  | ||||
|   Future<void> sync() { | ||||
|   // No need to cancel the task, as it can also be run when the user logs out | ||||
|   Future<void> syncLocal({bool full = false}) { | ||||
|     if (_deviceAlbumSyncTask != null) { | ||||
|       return _deviceAlbumSyncTask!.future; | ||||
|     } | ||||
|  | ||||
|     // We use a ternary operator to avoid [_deviceAlbumSyncTask] from being | ||||
|     // captured by the closure passed to [runInIsolateGentle]. | ||||
|     _deviceAlbumSyncTask = full | ||||
|         ? runInIsolateGentle( | ||||
|             computation: (ref) => | ||||
|                 ref.read(localSyncServiceProvider).sync(full: true), | ||||
|           ) | ||||
|         : runInIsolateGentle( | ||||
|             computation: (ref) => | ||||
|                 ref.read(localSyncServiceProvider).sync(full: false), | ||||
|           ); | ||||
|  | ||||
|     return _deviceAlbumSyncTask!.whenComplete(() { | ||||
|       _deviceAlbumSyncTask = null; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   Future<void> syncRemote() { | ||||
|     if (_syncTask != null) { | ||||
|       return _syncTask!.future; | ||||
|     } | ||||
| @@ -31,9 +53,8 @@ class BackgroundSyncManager { | ||||
|     _syncTask = runInIsolateGentle( | ||||
|       computation: (ref) => ref.read(syncStreamServiceProvider).sync(), | ||||
|     ); | ||||
|     _syncTask!.whenComplete(() { | ||||
|     return _syncTask!.whenComplete(() { | ||||
|       _syncTask = null; | ||||
|     }); | ||||
|     return _syncTask!.future; | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										18
									
								
								mobile/lib/infrastructure/entities/local_album.entity.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								mobile/lib/infrastructure/entities/local_album.entity.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import 'package:drift/drift.dart'; | ||||
| import 'package:immich_mobile/domain/models/local_album.model.dart'; | ||||
| import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; | ||||
|  | ||||
| class LocalAlbumEntity extends Table with DriftDefaultsMixin { | ||||
|   const LocalAlbumEntity(); | ||||
|  | ||||
|   TextColumn get id => text()(); | ||||
|   TextColumn get name => text()(); | ||||
|   DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); | ||||
|   IntColumn get backupSelection => intEnum<BackupSelection>()(); | ||||
|  | ||||
|   // Used for mark & sweep | ||||
|   BoolColumn get marker_ => boolean().nullable()(); | ||||
|  | ||||
|   @override | ||||
|   Set<Column> get primaryKey => {id}; | ||||
| } | ||||
							
								
								
									
										497
									
								
								mobile/lib/infrastructure/entities/local_album.entity.drift.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										497
									
								
								mobile/lib/infrastructure/entities/local_album.entity.drift.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,497 @@ | ||||
| // dart format width=80 | ||||
| // ignore_for_file: type=lint | ||||
| import 'package:drift/drift.dart' as i0; | ||||
| import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' | ||||
|     as i1; | ||||
| import 'package:immich_mobile/domain/models/local_album.model.dart' as i2; | ||||
| import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart' | ||||
|     as i3; | ||||
| import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; | ||||
| 
 | ||||
| typedef $$LocalAlbumEntityTableCreateCompanionBuilder | ||||
|     = i1.LocalAlbumEntityCompanion Function({ | ||||
|   required String id, | ||||
|   required String name, | ||||
|   i0.Value<DateTime> updatedAt, | ||||
|   required i2.BackupSelection backupSelection, | ||||
|   i0.Value<bool?> marker_, | ||||
| }); | ||||
| typedef $$LocalAlbumEntityTableUpdateCompanionBuilder | ||||
|     = i1.LocalAlbumEntityCompanion Function({ | ||||
|   i0.Value<String> id, | ||||
|   i0.Value<String> name, | ||||
|   i0.Value<DateTime> updatedAt, | ||||
|   i0.Value<i2.BackupSelection> backupSelection, | ||||
|   i0.Value<bool?> marker_, | ||||
| }); | ||||
| 
 | ||||
| class $$LocalAlbumEntityTableFilterComposer | ||||
|     extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAlbumEntityTable> { | ||||
|   $$LocalAlbumEntityTableFilterComposer({ | ||||
|     required super.$db, | ||||
|     required super.$table, | ||||
|     super.joinBuilder, | ||||
|     super.$addJoinBuilderToRootComposer, | ||||
|     super.$removeJoinBuilderFromRootComposer, | ||||
|   }); | ||||
|   i0.ColumnFilters<String> get id => $composableBuilder( | ||||
|       column: $table.id, builder: (column) => i0.ColumnFilters(column)); | ||||
| 
 | ||||
|   i0.ColumnFilters<String> get name => $composableBuilder( | ||||
|       column: $table.name, builder: (column) => i0.ColumnFilters(column)); | ||||
| 
 | ||||
|   i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder( | ||||
|       column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column)); | ||||
| 
 | ||||
|   i0.ColumnWithTypeConverterFilters<i2.BackupSelection, i2.BackupSelection, int> | ||||
|       get backupSelection => $composableBuilder( | ||||
|           column: $table.backupSelection, | ||||
|           builder: (column) => i0.ColumnWithTypeConverterFilters(column)); | ||||
| 
 | ||||
|   i0.ColumnFilters<bool> get marker_ => $composableBuilder( | ||||
|       column: $table.marker_, builder: (column) => i0.ColumnFilters(column)); | ||||
| } | ||||
| 
 | ||||
| class $$LocalAlbumEntityTableOrderingComposer | ||||
|     extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAlbumEntityTable> { | ||||
|   $$LocalAlbumEntityTableOrderingComposer({ | ||||
|     required super.$db, | ||||
|     required super.$table, | ||||
|     super.joinBuilder, | ||||
|     super.$addJoinBuilderToRootComposer, | ||||
|     super.$removeJoinBuilderFromRootComposer, | ||||
|   }); | ||||
|   i0.ColumnOrderings<String> get id => $composableBuilder( | ||||
|       column: $table.id, builder: (column) => i0.ColumnOrderings(column)); | ||||
| 
 | ||||
|   i0.ColumnOrderings<String> get name => $composableBuilder( | ||||
|       column: $table.name, builder: (column) => i0.ColumnOrderings(column)); | ||||
| 
 | ||||
|   i0.ColumnOrderings<DateTime> get updatedAt => $composableBuilder( | ||||
|       column: $table.updatedAt, | ||||
|       builder: (column) => i0.ColumnOrderings(column)); | ||||
| 
 | ||||
|   i0.ColumnOrderings<int> get backupSelection => $composableBuilder( | ||||
|       column: $table.backupSelection, | ||||
|       builder: (column) => i0.ColumnOrderings(column)); | ||||
| 
 | ||||
|   i0.ColumnOrderings<bool> get marker_ => $composableBuilder( | ||||
|       column: $table.marker_, builder: (column) => i0.ColumnOrderings(column)); | ||||
| } | ||||
| 
 | ||||
| class $$LocalAlbumEntityTableAnnotationComposer | ||||
|     extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAlbumEntityTable> { | ||||
|   $$LocalAlbumEntityTableAnnotationComposer({ | ||||
|     required super.$db, | ||||
|     required super.$table, | ||||
|     super.joinBuilder, | ||||
|     super.$addJoinBuilderToRootComposer, | ||||
|     super.$removeJoinBuilderFromRootComposer, | ||||
|   }); | ||||
|   i0.GeneratedColumn<String> get id => | ||||
|       $composableBuilder(column: $table.id, builder: (column) => column); | ||||
| 
 | ||||
|   i0.GeneratedColumn<String> get name => | ||||
|       $composableBuilder(column: $table.name, builder: (column) => column); | ||||
| 
 | ||||
|   i0.GeneratedColumn<DateTime> get updatedAt => | ||||
|       $composableBuilder(column: $table.updatedAt, builder: (column) => column); | ||||
| 
 | ||||
|   i0.GeneratedColumnWithTypeConverter<i2.BackupSelection, int> | ||||
|       get backupSelection => $composableBuilder( | ||||
|           column: $table.backupSelection, builder: (column) => column); | ||||
| 
 | ||||
|   i0.GeneratedColumn<bool> get marker_ => | ||||
|       $composableBuilder(column: $table.marker_, builder: (column) => column); | ||||
| } | ||||
| 
 | ||||
| class $$LocalAlbumEntityTableTableManager extends i0.RootTableManager< | ||||
|     i0.GeneratedDatabase, | ||||
|     i1.$LocalAlbumEntityTable, | ||||
|     i1.LocalAlbumEntityData, | ||||
|     i1.$$LocalAlbumEntityTableFilterComposer, | ||||
|     i1.$$LocalAlbumEntityTableOrderingComposer, | ||||
|     i1.$$LocalAlbumEntityTableAnnotationComposer, | ||||
|     $$LocalAlbumEntityTableCreateCompanionBuilder, | ||||
|     $$LocalAlbumEntityTableUpdateCompanionBuilder, | ||||
|     ( | ||||
|       i1.LocalAlbumEntityData, | ||||
|       i0.BaseReferences<i0.GeneratedDatabase, i1.$LocalAlbumEntityTable, | ||||
|           i1.LocalAlbumEntityData> | ||||
|     ), | ||||
|     i1.LocalAlbumEntityData, | ||||
|     i0.PrefetchHooks Function()> { | ||||
|   $$LocalAlbumEntityTableTableManager( | ||||
|       i0.GeneratedDatabase db, i1.$LocalAlbumEntityTable table) | ||||
|       : super(i0.TableManagerState( | ||||
|           db: db, | ||||
|           table: table, | ||||
|           createFilteringComposer: () => | ||||
|               i1.$$LocalAlbumEntityTableFilterComposer($db: db, $table: table), | ||||
|           createOrderingComposer: () => i1 | ||||
|               .$$LocalAlbumEntityTableOrderingComposer($db: db, $table: table), | ||||
|           createComputedFieldComposer: () => | ||||
|               i1.$$LocalAlbumEntityTableAnnotationComposer( | ||||
|                   $db: db, $table: table), | ||||
|           updateCompanionCallback: ({ | ||||
|             i0.Value<String> id = const i0.Value.absent(), | ||||
|             i0.Value<String> name = const i0.Value.absent(), | ||||
|             i0.Value<DateTime> updatedAt = const i0.Value.absent(), | ||||
|             i0.Value<i2.BackupSelection> backupSelection = | ||||
|                 const i0.Value.absent(), | ||||
|             i0.Value<bool?> marker_ = const i0.Value.absent(), | ||||
|           }) => | ||||
|               i1.LocalAlbumEntityCompanion( | ||||
|             id: id, | ||||
|             name: name, | ||||
|             updatedAt: updatedAt, | ||||
|             backupSelection: backupSelection, | ||||
|             marker_: marker_, | ||||
|           ), | ||||
|           createCompanionCallback: ({ | ||||
|             required String id, | ||||
|             required String name, | ||||
|             i0.Value<DateTime> updatedAt = const i0.Value.absent(), | ||||
|             required i2.BackupSelection backupSelection, | ||||
|             i0.Value<bool?> marker_ = const i0.Value.absent(), | ||||
|           }) => | ||||
|               i1.LocalAlbumEntityCompanion.insert( | ||||
|             id: id, | ||||
|             name: name, | ||||
|             updatedAt: updatedAt, | ||||
|             backupSelection: backupSelection, | ||||
|             marker_: marker_, | ||||
|           ), | ||||
|           withReferenceMapper: (p0) => p0 | ||||
|               .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) | ||||
|               .toList(), | ||||
|           prefetchHooksCallback: null, | ||||
|         )); | ||||
| } | ||||
| 
 | ||||
| typedef $$LocalAlbumEntityTableProcessedTableManager = i0.ProcessedTableManager< | ||||
|     i0.GeneratedDatabase, | ||||
|     i1.$LocalAlbumEntityTable, | ||||
|     i1.LocalAlbumEntityData, | ||||
|     i1.$$LocalAlbumEntityTableFilterComposer, | ||||
|     i1.$$LocalAlbumEntityTableOrderingComposer, | ||||
|     i1.$$LocalAlbumEntityTableAnnotationComposer, | ||||
|     $$LocalAlbumEntityTableCreateCompanionBuilder, | ||||
|     $$LocalAlbumEntityTableUpdateCompanionBuilder, | ||||
|     ( | ||||
|       i1.LocalAlbumEntityData, | ||||
|       i0.BaseReferences<i0.GeneratedDatabase, i1.$LocalAlbumEntityTable, | ||||
|           i1.LocalAlbumEntityData> | ||||
|     ), | ||||
|     i1.LocalAlbumEntityData, | ||||
|     i0.PrefetchHooks Function()>; | ||||
| 
 | ||||
| class $LocalAlbumEntityTable extends i3.LocalAlbumEntity | ||||
|     with i0.TableInfo<$LocalAlbumEntityTable, i1.LocalAlbumEntityData> { | ||||
|   @override | ||||
|   final i0.GeneratedDatabase attachedDatabase; | ||||
|   final String? _alias; | ||||
|   $LocalAlbumEntityTable(this.attachedDatabase, [this._alias]); | ||||
|   static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); | ||||
|   @override | ||||
|   late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>( | ||||
|       'id', aliasedName, false, | ||||
|       type: i0.DriftSqlType.string, requiredDuringInsert: true); | ||||
|   static const i0.VerificationMeta _nameMeta = | ||||
|       const i0.VerificationMeta('name'); | ||||
|   @override | ||||
|   late final i0.GeneratedColumn<String> name = i0.GeneratedColumn<String>( | ||||
|       'name', aliasedName, false, | ||||
|       type: i0.DriftSqlType.string, requiredDuringInsert: true); | ||||
|   static const i0.VerificationMeta _updatedAtMeta = | ||||
|       const i0.VerificationMeta('updatedAt'); | ||||
|   @override | ||||
|   late final i0.GeneratedColumn<DateTime> updatedAt = | ||||
|       i0.GeneratedColumn<DateTime>('updated_at', aliasedName, false, | ||||
|           type: i0.DriftSqlType.dateTime, | ||||
|           requiredDuringInsert: false, | ||||
|           defaultValue: i4.currentDateAndTime); | ||||
|   @override | ||||
|   late final i0.GeneratedColumnWithTypeConverter<i2.BackupSelection, int> | ||||
|       backupSelection = i0.GeneratedColumn<int>( | ||||
|               'backup_selection', aliasedName, false, | ||||
|               type: i0.DriftSqlType.int, requiredDuringInsert: true) | ||||
|           .withConverter<i2.BackupSelection>( | ||||
|               i1.$LocalAlbumEntityTable.$converterbackupSelection); | ||||
|   static const i0.VerificationMeta _marker_Meta = | ||||
|       const i0.VerificationMeta('marker_'); | ||||
|   @override | ||||
|   late final i0.GeneratedColumn<bool> marker_ = i0.GeneratedColumn<bool>( | ||||
|       'marker', aliasedName, true, | ||||
|       type: i0.DriftSqlType.bool, | ||||
|       requiredDuringInsert: false, | ||||
|       defaultConstraints: | ||||
|           i0.GeneratedColumn.constraintIsAlways('CHECK ("marker" IN (0, 1))')); | ||||
|   @override | ||||
|   List<i0.GeneratedColumn> get $columns => | ||||
|       [id, name, updatedAt, backupSelection, marker_]; | ||||
|   @override | ||||
|   String get aliasedName => _alias ?? actualTableName; | ||||
|   @override | ||||
|   String get actualTableName => $name; | ||||
|   static const String $name = 'local_album_entity'; | ||||
|   @override | ||||
|   i0.VerificationContext validateIntegrity( | ||||
|       i0.Insertable<i1.LocalAlbumEntityData> instance, | ||||
|       {bool isInserting = false}) { | ||||
|     final context = i0.VerificationContext(); | ||||
|     final data = instance.toColumns(true); | ||||
|     if (data.containsKey('id')) { | ||||
|       context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); | ||||
|     } else if (isInserting) { | ||||
|       context.missing(_idMeta); | ||||
|     } | ||||
|     if (data.containsKey('name')) { | ||||
|       context.handle( | ||||
|           _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); | ||||
|     } else if (isInserting) { | ||||
|       context.missing(_nameMeta); | ||||
|     } | ||||
|     if (data.containsKey('updated_at')) { | ||||
|       context.handle(_updatedAtMeta, | ||||
|           updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); | ||||
|     } | ||||
|     if (data.containsKey('marker')) { | ||||
|       context.handle(_marker_Meta, | ||||
|           marker_.isAcceptableOrUnknown(data['marker']!, _marker_Meta)); | ||||
|     } | ||||
|     return context; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Set<i0.GeneratedColumn> get $primaryKey => {id}; | ||||
|   @override | ||||
|   i1.LocalAlbumEntityData map(Map<String, dynamic> data, | ||||
|       {String? tablePrefix}) { | ||||
|     final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; | ||||
|     return i1.LocalAlbumEntityData( | ||||
|       id: attachedDatabase.typeMapping | ||||
|           .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, | ||||
|       name: attachedDatabase.typeMapping | ||||
|           .read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!, | ||||
|       updatedAt: attachedDatabase.typeMapping.read( | ||||
|           i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, | ||||
|       backupSelection: i1.$LocalAlbumEntityTable.$converterbackupSelection | ||||
|           .fromSql(attachedDatabase.typeMapping.read(i0.DriftSqlType.int, | ||||
|               data['${effectivePrefix}backup_selection'])!), | ||||
|       marker_: attachedDatabase.typeMapping | ||||
|           .read(i0.DriftSqlType.bool, data['${effectivePrefix}marker']), | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   $LocalAlbumEntityTable createAlias(String alias) { | ||||
|     return $LocalAlbumEntityTable(attachedDatabase, alias); | ||||
|   } | ||||
| 
 | ||||
|   static i0.JsonTypeConverter2<i2.BackupSelection, int, int> | ||||
|       $converterbackupSelection = | ||||
|       const i0.EnumIndexConverter<i2.BackupSelection>( | ||||
|           i2.BackupSelection.values); | ||||
|   @override | ||||
|   bool get withoutRowId => true; | ||||
|   @override | ||||
|   bool get isStrict => true; | ||||
| } | ||||
| 
 | ||||
| class LocalAlbumEntityData extends i0.DataClass | ||||
|     implements i0.Insertable<i1.LocalAlbumEntityData> { | ||||
|   final String id; | ||||
|   final String name; | ||||
|   final DateTime updatedAt; | ||||
|   final i2.BackupSelection backupSelection; | ||||
|   final bool? marker_; | ||||
|   const LocalAlbumEntityData( | ||||
|       {required this.id, | ||||
|       required this.name, | ||||
|       required this.updatedAt, | ||||
|       required this.backupSelection, | ||||
|       this.marker_}); | ||||
|   @override | ||||
|   Map<String, i0.Expression> toColumns(bool nullToAbsent) { | ||||
|     final map = <String, i0.Expression>{}; | ||||
|     map['id'] = i0.Variable<String>(id); | ||||
|     map['name'] = i0.Variable<String>(name); | ||||
|     map['updated_at'] = i0.Variable<DateTime>(updatedAt); | ||||
|     { | ||||
|       map['backup_selection'] = i0.Variable<int>(i1 | ||||
|           .$LocalAlbumEntityTable.$converterbackupSelection | ||||
|           .toSql(backupSelection)); | ||||
|     } | ||||
|     if (!nullToAbsent || marker_ != null) { | ||||
|       map['marker'] = i0.Variable<bool>(marker_); | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   factory LocalAlbumEntityData.fromJson(Map<String, dynamic> json, | ||||
|       {i0.ValueSerializer? serializer}) { | ||||
|     serializer ??= i0.driftRuntimeOptions.defaultSerializer; | ||||
|     return LocalAlbumEntityData( | ||||
|       id: serializer.fromJson<String>(json['id']), | ||||
|       name: serializer.fromJson<String>(json['name']), | ||||
|       updatedAt: serializer.fromJson<DateTime>(json['updatedAt']), | ||||
|       backupSelection: i1.$LocalAlbumEntityTable.$converterbackupSelection | ||||
|           .fromJson(serializer.fromJson<int>(json['backupSelection'])), | ||||
|       marker_: serializer.fromJson<bool?>(json['marker_']), | ||||
|     ); | ||||
|   } | ||||
|   @override | ||||
|   Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) { | ||||
|     serializer ??= i0.driftRuntimeOptions.defaultSerializer; | ||||
|     return <String, dynamic>{ | ||||
|       'id': serializer.toJson<String>(id), | ||||
|       'name': serializer.toJson<String>(name), | ||||
|       'updatedAt': serializer.toJson<DateTime>(updatedAt), | ||||
|       'backupSelection': serializer.toJson<int>(i1 | ||||
|           .$LocalAlbumEntityTable.$converterbackupSelection | ||||
|           .toJson(backupSelection)), | ||||
|       'marker_': serializer.toJson<bool?>(marker_), | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   i1.LocalAlbumEntityData copyWith( | ||||
|           {String? id, | ||||
|           String? name, | ||||
|           DateTime? updatedAt, | ||||
|           i2.BackupSelection? backupSelection, | ||||
|           i0.Value<bool?> marker_ = const i0.Value.absent()}) => | ||||
|       i1.LocalAlbumEntityData( | ||||
|         id: id ?? this.id, | ||||
|         name: name ?? this.name, | ||||
|         updatedAt: updatedAt ?? this.updatedAt, | ||||
|         backupSelection: backupSelection ?? this.backupSelection, | ||||
|         marker_: marker_.present ? marker_.value : this.marker_, | ||||
|       ); | ||||
|   LocalAlbumEntityData copyWithCompanion(i1.LocalAlbumEntityCompanion data) { | ||||
|     return LocalAlbumEntityData( | ||||
|       id: data.id.present ? data.id.value : this.id, | ||||
|       name: data.name.present ? data.name.value : this.name, | ||||
|       updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, | ||||
|       backupSelection: data.backupSelection.present | ||||
|           ? data.backupSelection.value | ||||
|           : this.backupSelection, | ||||
|       marker_: data.marker_.present ? data.marker_.value : this.marker_, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   String toString() { | ||||
|     return (StringBuffer('LocalAlbumEntityData(') | ||||
|           ..write('id: $id, ') | ||||
|           ..write('name: $name, ') | ||||
|           ..write('updatedAt: $updatedAt, ') | ||||
|           ..write('backupSelection: $backupSelection, ') | ||||
|           ..write('marker_: $marker_') | ||||
|           ..write(')')) | ||||
|         .toString(); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|       Object.hash(id, name, updatedAt, backupSelection, marker_); | ||||
|   @override | ||||
|   bool operator ==(Object other) => | ||||
|       identical(this, other) || | ||||
|       (other is i1.LocalAlbumEntityData && | ||||
|           other.id == this.id && | ||||
|           other.name == this.name && | ||||
|           other.updatedAt == this.updatedAt && | ||||
|           other.backupSelection == this.backupSelection && | ||||
|           other.marker_ == this.marker_); | ||||
| } | ||||
| 
 | ||||
| class LocalAlbumEntityCompanion | ||||
|     extends i0.UpdateCompanion<i1.LocalAlbumEntityData> { | ||||
|   final i0.Value<String> id; | ||||
|   final i0.Value<String> name; | ||||
|   final i0.Value<DateTime> updatedAt; | ||||
|   final i0.Value<i2.BackupSelection> backupSelection; | ||||
|   final i0.Value<bool?> marker_; | ||||
|   const LocalAlbumEntityCompanion({ | ||||
|     this.id = const i0.Value.absent(), | ||||
|     this.name = const i0.Value.absent(), | ||||
|     this.updatedAt = const i0.Value.absent(), | ||||
|     this.backupSelection = const i0.Value.absent(), | ||||
|     this.marker_ = const i0.Value.absent(), | ||||
|   }); | ||||
|   LocalAlbumEntityCompanion.insert({ | ||||
|     required String id, | ||||
|     required String name, | ||||
|     this.updatedAt = const i0.Value.absent(), | ||||
|     required i2.BackupSelection backupSelection, | ||||
|     this.marker_ = const i0.Value.absent(), | ||||
|   })  : id = i0.Value(id), | ||||
|         name = i0.Value(name), | ||||
|         backupSelection = i0.Value(backupSelection); | ||||
|   static i0.Insertable<i1.LocalAlbumEntityData> custom({ | ||||
|     i0.Expression<String>? id, | ||||
|     i0.Expression<String>? name, | ||||
|     i0.Expression<DateTime>? updatedAt, | ||||
|     i0.Expression<int>? backupSelection, | ||||
|     i0.Expression<bool>? marker_, | ||||
|   }) { | ||||
|     return i0.RawValuesInsertable({ | ||||
|       if (id != null) 'id': id, | ||||
|       if (name != null) 'name': name, | ||||
|       if (updatedAt != null) 'updated_at': updatedAt, | ||||
|       if (backupSelection != null) 'backup_selection': backupSelection, | ||||
|       if (marker_ != null) 'marker': marker_, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   i1.LocalAlbumEntityCompanion copyWith( | ||||
|       {i0.Value<String>? id, | ||||
|       i0.Value<String>? name, | ||||
|       i0.Value<DateTime>? updatedAt, | ||||
|       i0.Value<i2.BackupSelection>? backupSelection, | ||||
|       i0.Value<bool?>? marker_}) { | ||||
|     return i1.LocalAlbumEntityCompanion( | ||||
|       id: id ?? this.id, | ||||
|       name: name ?? this.name, | ||||
|       updatedAt: updatedAt ?? this.updatedAt, | ||||
|       backupSelection: backupSelection ?? this.backupSelection, | ||||
|       marker_: marker_ ?? this.marker_, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Map<String, i0.Expression> toColumns(bool nullToAbsent) { | ||||
|     final map = <String, i0.Expression>{}; | ||||
|     if (id.present) { | ||||
|       map['id'] = i0.Variable<String>(id.value); | ||||
|     } | ||||
|     if (name.present) { | ||||
|       map['name'] = i0.Variable<String>(name.value); | ||||
|     } | ||||
|     if (updatedAt.present) { | ||||
|       map['updated_at'] = i0.Variable<DateTime>(updatedAt.value); | ||||
|     } | ||||
|     if (backupSelection.present) { | ||||
|       map['backup_selection'] = i0.Variable<int>(i1 | ||||
|           .$LocalAlbumEntityTable.$converterbackupSelection | ||||
|           .toSql(backupSelection.value)); | ||||
|     } | ||||
|     if (marker_.present) { | ||||
|       map['marker'] = i0.Variable<bool>(marker_.value); | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   String toString() { | ||||
|     return (StringBuffer('LocalAlbumEntityCompanion(') | ||||
|           ..write('id: $id, ') | ||||
|           ..write('name: $name, ') | ||||
|           ..write('updatedAt: $updatedAt, ') | ||||
|           ..write('backupSelection: $backupSelection, ') | ||||
|           ..write('marker_: $marker_') | ||||
|           ..write(')')) | ||||
|         .toString(); | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,17 @@ | ||||
| import 'package:drift/drift.dart'; | ||||
| import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; | ||||
| import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; | ||||
| import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; | ||||
|  | ||||
| class LocalAlbumAssetEntity extends Table with DriftDefaultsMixin { | ||||
|   const LocalAlbumAssetEntity(); | ||||
|  | ||||
|   TextColumn get assetId => | ||||
|       text().references(LocalAssetEntity, #id, onDelete: KeyAction.cascade)(); | ||||
|  | ||||
|   TextColumn get albumId => | ||||
|       text().references(LocalAlbumEntity, #id, onDelete: KeyAction.cascade)(); | ||||
|  | ||||
|   @override | ||||
|   Set<Column> get primaryKey => {assetId, albumId}; | ||||
| } | ||||
							
								
								
									
										565
									
								
								mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										565
									
								
								mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,565 @@ | ||||
| // dart format width=80 | ||||
| // ignore_for_file: type=lint | ||||
| import 'package:drift/drift.dart' as i0; | ||||
| import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart' | ||||
|     as i1; | ||||
| import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart' | ||||
|     as i2; | ||||
| import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' | ||||
|     as i3; | ||||
| import 'package:drift/internal/modular.dart' as i4; | ||||
| import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' | ||||
|     as i5; | ||||
| 
 | ||||
| typedef $$LocalAlbumAssetEntityTableCreateCompanionBuilder | ||||
|     = i1.LocalAlbumAssetEntityCompanion Function({ | ||||
|   required String assetId, | ||||
|   required String albumId, | ||||
| }); | ||||
| typedef $$LocalAlbumAssetEntityTableUpdateCompanionBuilder | ||||
|     = i1.LocalAlbumAssetEntityCompanion Function({ | ||||
|   i0.Value<String> assetId, | ||||
|   i0.Value<String> albumId, | ||||
| }); | ||||
| 
 | ||||
| final class $$LocalAlbumAssetEntityTableReferences extends i0.BaseReferences< | ||||
|     i0.GeneratedDatabase, | ||||
|     i1.$LocalAlbumAssetEntityTable, | ||||
|     i1.LocalAlbumAssetEntityData> { | ||||
|   $$LocalAlbumAssetEntityTableReferences( | ||||
|       super.$_db, super.$_table, super.$_typedResult); | ||||
| 
 | ||||
|   static i3.$LocalAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) => | ||||
|       i4.ReadDatabaseContainer(db) | ||||
|           .resultSet<i3.$LocalAssetEntityTable>('local_asset_entity') | ||||
|           .createAlias(i0.$_aliasNameGenerator( | ||||
|               i4.ReadDatabaseContainer(db) | ||||
|                   .resultSet<i1.$LocalAlbumAssetEntityTable>( | ||||
|                       'local_album_asset_entity') | ||||
|                   .assetId, | ||||
|               i4.ReadDatabaseContainer(db) | ||||
|                   .resultSet<i3.$LocalAssetEntityTable>('local_asset_entity') | ||||
|                   .id)); | ||||
| 
 | ||||
|   i3.$$LocalAssetEntityTableProcessedTableManager get assetId { | ||||
|     final $_column = $_itemColumn<String>('asset_id')!; | ||||
| 
 | ||||
|     final manager = i3 | ||||
|         .$$LocalAssetEntityTableTableManager( | ||||
|             $_db, | ||||
|             i4.ReadDatabaseContainer($_db) | ||||
|                 .resultSet<i3.$LocalAssetEntityTable>('local_asset_entity')) | ||||
|         .filter((f) => f.id.sqlEquals($_column)); | ||||
|     final item = $_typedResult.readTableOrNull(_assetIdTable($_db)); | ||||
|     if (item == null) return manager; | ||||
|     return i0.ProcessedTableManager( | ||||
|         manager.$state.copyWith(prefetchedData: [item])); | ||||
|   } | ||||
| 
 | ||||
|   static i5.$LocalAlbumEntityTable _albumIdTable(i0.GeneratedDatabase db) => | ||||
|       i4.ReadDatabaseContainer(db) | ||||
|           .resultSet<i5.$LocalAlbumEntityTable>('local_album_entity') | ||||
|           .createAlias(i0.$_aliasNameGenerator( | ||||
|               i4.ReadDatabaseContainer(db) | ||||
|                   .resultSet<i1.$LocalAlbumAssetEntityTable>( | ||||
|                       'local_album_asset_entity') | ||||
|                   .albumId, | ||||
|               i4.ReadDatabaseContainer(db) | ||||
|                   .resultSet<i5.$LocalAlbumEntityTable>('local_album_entity') | ||||
|                   .id)); | ||||
| 
 | ||||
|   i5.$$LocalAlbumEntityTableProcessedTableManager get albumId { | ||||
|     final $_column = $_itemColumn<String>('album_id')!; | ||||
| 
 | ||||
|     final manager = i5 | ||||
|         .$$LocalAlbumEntityTableTableManager( | ||||
|             $_db, | ||||
|             i4.ReadDatabaseContainer($_db) | ||||
|                 .resultSet<i5.$LocalAlbumEntityTable>('local_album_entity')) | ||||
|         .filter((f) => f.id.sqlEquals($_column)); | ||||
|     final item = $_typedResult.readTableOrNull(_albumIdTable($_db)); | ||||
|     if (item == null) return manager; | ||||
|     return i0.ProcessedTableManager( | ||||
|         manager.$state.copyWith(prefetchedData: [item])); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class $$LocalAlbumAssetEntityTableFilterComposer | ||||
|     extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAlbumAssetEntityTable> { | ||||
|   $$LocalAlbumAssetEntityTableFilterComposer({ | ||||
|     required super.$db, | ||||
|     required super.$table, | ||||
|     super.joinBuilder, | ||||
|     super.$addJoinBuilderToRootComposer, | ||||
|     super.$removeJoinBuilderFromRootComposer, | ||||
|   }); | ||||
|   i3.$$LocalAssetEntityTableFilterComposer get assetId { | ||||
|     final i3.$$LocalAssetEntityTableFilterComposer composer = $composerBuilder( | ||||
|         composer: this, | ||||
|         getCurrentColumn: (t) => t.assetId, | ||||
|         referencedTable: i4.ReadDatabaseContainer($db) | ||||
|             .resultSet<i3.$LocalAssetEntityTable>('local_asset_entity'), | ||||
|         getReferencedColumn: (t) => t.id, | ||||
|         builder: (joinBuilder, | ||||
|                 {$addJoinBuilderToRootComposer, | ||||
|                 $removeJoinBuilderFromRootComposer}) => | ||||
|             i3.$$LocalAssetEntityTableFilterComposer( | ||||
|               $db: $db, | ||||
|               $table: i4.ReadDatabaseContainer($db) | ||||
|                   .resultSet<i3.$LocalAssetEntityTable>('local_asset_entity'), | ||||
|               $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, | ||||
|               joinBuilder: joinBuilder, | ||||
|               $removeJoinBuilderFromRootComposer: | ||||
|                   $removeJoinBuilderFromRootComposer, | ||||
|             )); | ||||
|     return composer; | ||||
|   } | ||||
| 
 | ||||
|   i5.$$LocalAlbumEntityTableFilterComposer get albumId { | ||||
|     final i5.$$LocalAlbumEntityTableFilterComposer composer = $composerBuilder( | ||||
|         composer: this, | ||||
|         getCurrentColumn: (t) => t.albumId, | ||||
|         referencedTable: i4.ReadDatabaseContainer($db) | ||||
|             .resultSet<i5.$LocalAlbumEntityTable>('local_album_entity'), | ||||
|         getReferencedColumn: (t) => t.id, | ||||
|         builder: (joinBuilder, | ||||
|                 {$addJoinBuilderToRootComposer, | ||||
|                 $removeJoinBuilderFromRootComposer}) => | ||||
|             i5.$$LocalAlbumEntityTableFilterComposer( | ||||
|               $db: $db, | ||||
|               $table: i4.ReadDatabaseContainer($db) | ||||
|                   .resultSet<i5.$LocalAlbumEntityTable>('local_album_entity'), | ||||
|               $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, | ||||
|               joinBuilder: joinBuilder, | ||||
|               $removeJoinBuilderFromRootComposer: | ||||
|                   $removeJoinBuilderFromRootComposer, | ||||
|             )); | ||||
|     return composer; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class $$LocalAlbumAssetEntityTableOrderingComposer | ||||
|     extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAlbumAssetEntityTable> { | ||||
|   $$LocalAlbumAssetEntityTableOrderingComposer({ | ||||
|     required super.$db, | ||||
|     required super.$table, | ||||
|     super.joinBuilder, | ||||
|     super.$addJoinBuilderToRootComposer, | ||||
|     super.$removeJoinBuilderFromRootComposer, | ||||
|   }); | ||||
|   i3.$$LocalAssetEntityTableOrderingComposer get assetId { | ||||
|     final i3.$$LocalAssetEntityTableOrderingComposer composer = | ||||
|         $composerBuilder( | ||||
|             composer: this, | ||||
|             getCurrentColumn: (t) => t.assetId, | ||||
|             referencedTable: i4.ReadDatabaseContainer($db) | ||||
|                 .resultSet<i3.$LocalAssetEntityTable>('local_asset_entity'), | ||||
|             getReferencedColumn: (t) => t.id, | ||||
|             builder: (joinBuilder, | ||||
|                     {$addJoinBuilderToRootComposer, | ||||
|                     $removeJoinBuilderFromRootComposer}) => | ||||
|                 i3.$$LocalAssetEntityTableOrderingComposer( | ||||
|                   $db: $db, | ||||
|                   $table: i4.ReadDatabaseContainer($db) | ||||
|                       .resultSet<i3.$LocalAssetEntityTable>( | ||||
|                           'local_asset_entity'), | ||||
|                   $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, | ||||
|                   joinBuilder: joinBuilder, | ||||
|                   $removeJoinBuilderFromRootComposer: | ||||
|                       $removeJoinBuilderFromRootComposer, | ||||
|                 )); | ||||
|     return composer; | ||||
|   } | ||||
| 
 | ||||
|   i5.$$LocalAlbumEntityTableOrderingComposer get albumId { | ||||
|     final i5.$$LocalAlbumEntityTableOrderingComposer composer = | ||||
|         $composerBuilder( | ||||
|             composer: this, | ||||
|             getCurrentColumn: (t) => t.albumId, | ||||
|             referencedTable: i4.ReadDatabaseContainer($db) | ||||
|                 .resultSet<i5.$LocalAlbumEntityTable>('local_album_entity'), | ||||
|             getReferencedColumn: (t) => t.id, | ||||
|             builder: (joinBuilder, | ||||
|                     {$addJoinBuilderToRootComposer, | ||||
|                     $removeJoinBuilderFromRootComposer}) => | ||||
|                 i5.$$LocalAlbumEntityTableOrderingComposer( | ||||
|                   $db: $db, | ||||
|                   $table: i4.ReadDatabaseContainer($db) | ||||
|                       .resultSet<i5.$LocalAlbumEntityTable>( | ||||
|                           'local_album_entity'), | ||||
|                   $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, | ||||
|                   joinBuilder: joinBuilder, | ||||
|                   $removeJoinBuilderFromRootComposer: | ||||
|                       $removeJoinBuilderFromRootComposer, | ||||
|                 )); | ||||
|     return composer; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class $$LocalAlbumAssetEntityTableAnnotationComposer | ||||
|     extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAlbumAssetEntityTable> { | ||||
|   $$LocalAlbumAssetEntityTableAnnotationComposer({ | ||||
|     required super.$db, | ||||
|     required super.$table, | ||||
|     super.joinBuilder, | ||||
|     super.$addJoinBuilderToRootComposer, | ||||
|     super.$removeJoinBuilderFromRootComposer, | ||||
|   }); | ||||
|   i3.$$LocalAssetEntityTableAnnotationComposer get assetId { | ||||
|     final i3.$$LocalAssetEntityTableAnnotationComposer composer = | ||||
|         $composerBuilder( | ||||
|             composer: this, | ||||
|             getCurrentColumn: (t) => t.assetId, | ||||
|             referencedTable: i4.ReadDatabaseContainer($db) | ||||
|                 .resultSet<i3.$LocalAssetEntityTable>('local_asset_entity'), | ||||
|             getReferencedColumn: (t) => t.id, | ||||
|             builder: (joinBuilder, | ||||
|                     {$addJoinBuilderToRootComposer, | ||||
|                     $removeJoinBuilderFromRootComposer}) => | ||||
|                 i3.$$LocalAssetEntityTableAnnotationComposer( | ||||
|                   $db: $db, | ||||
|                   $table: i4.ReadDatabaseContainer($db) | ||||
|                       .resultSet<i3.$LocalAssetEntityTable>( | ||||
|                           'local_asset_entity'), | ||||
|                   $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, | ||||
|                   joinBuilder: joinBuilder, | ||||
|                   $removeJoinBuilderFromRootComposer: | ||||
|                       $removeJoinBuilderFromRootComposer, | ||||
|                 )); | ||||
|     return composer; | ||||
|   } | ||||
| 
 | ||||
|   i5.$$LocalAlbumEntityTableAnnotationComposer get albumId { | ||||
|     final i5.$$LocalAlbumEntityTableAnnotationComposer composer = | ||||
|         $composerBuilder( | ||||
|             composer: this, | ||||
|             getCurrentColumn: (t) => t.albumId, | ||||
|             referencedTable: i4.ReadDatabaseContainer($db) | ||||
|                 .resultSet<i5.$LocalAlbumEntityTable>('local_album_entity'), | ||||
|             getReferencedColumn: (t) => t.id, | ||||
|             builder: (joinBuilder, | ||||
|                     {$addJoinBuilderToRootComposer, | ||||
|                     $removeJoinBuilderFromRootComposer}) => | ||||
|                 i5.$$LocalAlbumEntityTableAnnotationComposer( | ||||
|                   $db: $db, | ||||
|                   $table: i4.ReadDatabaseContainer($db) | ||||
|                       .resultSet<i5.$LocalAlbumEntityTable>( | ||||
|                           'local_album_entity'), | ||||
|                   $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, | ||||
|                   joinBuilder: joinBuilder, | ||||
|                   $removeJoinBuilderFromRootComposer: | ||||
|                       $removeJoinBuilderFromRootComposer, | ||||
|                 )); | ||||
|     return composer; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class $$LocalAlbumAssetEntityTableTableManager extends i0.RootTableManager< | ||||
|     i0.GeneratedDatabase, | ||||
|     i1.$LocalAlbumAssetEntityTable, | ||||
|     i1.LocalAlbumAssetEntityData, | ||||
|     i1.$$LocalAlbumAssetEntityTableFilterComposer, | ||||
|     i1.$$LocalAlbumAssetEntityTableOrderingComposer, | ||||
|     i1.$$LocalAlbumAssetEntityTableAnnotationComposer, | ||||
|     $$LocalAlbumAssetEntityTableCreateCompanionBuilder, | ||||
|     $$LocalAlbumAssetEntityTableUpdateCompanionBuilder, | ||||
|     (i1.LocalAlbumAssetEntityData, i1.$$LocalAlbumAssetEntityTableReferences), | ||||
|     i1.LocalAlbumAssetEntityData, | ||||
|     i0.PrefetchHooks Function({bool assetId, bool albumId})> { | ||||
|   $$LocalAlbumAssetEntityTableTableManager( | ||||
|       i0.GeneratedDatabase db, i1.$LocalAlbumAssetEntityTable table) | ||||
|       : super(i0.TableManagerState( | ||||
|           db: db, | ||||
|           table: table, | ||||
|           createFilteringComposer: () => | ||||
|               i1.$$LocalAlbumAssetEntityTableFilterComposer( | ||||
|                   $db: db, $table: table), | ||||
|           createOrderingComposer: () => | ||||
|               i1.$$LocalAlbumAssetEntityTableOrderingComposer( | ||||
|                   $db: db, $table: table), | ||||
|           createComputedFieldComposer: () => | ||||
|               i1.$$LocalAlbumAssetEntityTableAnnotationComposer( | ||||
|                   $db: db, $table: table), | ||||
|           updateCompanionCallback: ({ | ||||
|             i0.Value<String> assetId = const i0.Value.absent(), | ||||
|             i0.Value<String> albumId = const i0.Value.absent(), | ||||
|           }) => | ||||
|               i1.LocalAlbumAssetEntityCompanion( | ||||
|             assetId: assetId, | ||||
|             albumId: albumId, | ||||
|           ), | ||||
|           createCompanionCallback: ({ | ||||
|             required String assetId, | ||||
|             required String albumId, | ||||
|           }) => | ||||
|               i1.LocalAlbumAssetEntityCompanion.insert( | ||||
|             assetId: assetId, | ||||
|             albumId: albumId, | ||||
|           ), | ||||
|           withReferenceMapper: (p0) => p0 | ||||
|               .map((e) => ( | ||||
|                     e.readTable(table), | ||||
|                     i1.$$LocalAlbumAssetEntityTableReferences(db, table, e) | ||||
|                   )) | ||||
|               .toList(), | ||||
|           prefetchHooksCallback: ({assetId = false, albumId = false}) { | ||||
|             return i0.PrefetchHooks( | ||||
|               db: db, | ||||
|               explicitlyWatchedTables: [], | ||||
|               addJoins: < | ||||
|                   T extends i0.TableManagerState< | ||||
|                       dynamic, | ||||
|                       dynamic, | ||||
|                       dynamic, | ||||
|                       dynamic, | ||||
|                       dynamic, | ||||
|                       dynamic, | ||||
|                       dynamic, | ||||
|                       dynamic, | ||||
|                       dynamic, | ||||
|                       dynamic, | ||||
|                       dynamic>>(state) { | ||||
|                 if (assetId) { | ||||
|                   state = state.withJoin( | ||||
|                     currentTable: table, | ||||
|                     currentColumn: table.assetId, | ||||
|                     referencedTable: i1.$$LocalAlbumAssetEntityTableReferences | ||||
|                         ._assetIdTable(db), | ||||
|                     referencedColumn: i1.$$LocalAlbumAssetEntityTableReferences | ||||
|                         ._assetIdTable(db) | ||||
|                         .id, | ||||
|                   ) as T; | ||||
|                 } | ||||
|                 if (albumId) { | ||||
|                   state = state.withJoin( | ||||
|                     currentTable: table, | ||||
|                     currentColumn: table.albumId, | ||||
|                     referencedTable: i1.$$LocalAlbumAssetEntityTableReferences | ||||
|                         ._albumIdTable(db), | ||||
|                     referencedColumn: i1.$$LocalAlbumAssetEntityTableReferences | ||||
|                         ._albumIdTable(db) | ||||
|                         .id, | ||||
|                   ) as T; | ||||
|                 } | ||||
| 
 | ||||
|                 return state; | ||||
|               }, | ||||
|               getPrefetchedDataCallback: (items) async { | ||||
|                 return []; | ||||
|               }, | ||||
|             ); | ||||
|           }, | ||||
|         )); | ||||
| } | ||||
| 
 | ||||
| typedef $$LocalAlbumAssetEntityTableProcessedTableManager | ||||
|     = i0.ProcessedTableManager< | ||||
|         i0.GeneratedDatabase, | ||||
|         i1.$LocalAlbumAssetEntityTable, | ||||
|         i1.LocalAlbumAssetEntityData, | ||||
|         i1.$$LocalAlbumAssetEntityTableFilterComposer, | ||||
|         i1.$$LocalAlbumAssetEntityTableOrderingComposer, | ||||
|         i1.$$LocalAlbumAssetEntityTableAnnotationComposer, | ||||
|         $$LocalAlbumAssetEntityTableCreateCompanionBuilder, | ||||
|         $$LocalAlbumAssetEntityTableUpdateCompanionBuilder, | ||||
|         ( | ||||
|           i1.LocalAlbumAssetEntityData, | ||||
|           i1.$$LocalAlbumAssetEntityTableReferences | ||||
|         ), | ||||
|         i1.LocalAlbumAssetEntityData, | ||||
|         i0.PrefetchHooks Function({bool assetId, bool albumId})>; | ||||
| 
 | ||||
| class $LocalAlbumAssetEntityTable extends i2.LocalAlbumAssetEntity | ||||
|     with | ||||
|         i0 | ||||
|         .TableInfo<$LocalAlbumAssetEntityTable, i1.LocalAlbumAssetEntityData> { | ||||
|   @override | ||||
|   final i0.GeneratedDatabase attachedDatabase; | ||||
|   final String? _alias; | ||||
|   $LocalAlbumAssetEntityTable(this.attachedDatabase, [this._alias]); | ||||
|   static const i0.VerificationMeta _assetIdMeta = | ||||
|       const i0.VerificationMeta('assetId'); | ||||
|   @override | ||||
|   late final i0.GeneratedColumn<String> assetId = i0.GeneratedColumn<String>( | ||||
|       'asset_id', aliasedName, false, | ||||
|       type: i0.DriftSqlType.string, | ||||
|       requiredDuringInsert: true, | ||||
|       defaultConstraints: i0.GeneratedColumn.constraintIsAlways( | ||||
|           'REFERENCES local_asset_entity (id) ON DELETE CASCADE')); | ||||
|   static const i0.VerificationMeta _albumIdMeta = | ||||
|       const i0.VerificationMeta('albumId'); | ||||
|   @override | ||||
|   late final i0.GeneratedColumn<String> albumId = i0.GeneratedColumn<String>( | ||||
|       'album_id', aliasedName, false, | ||||
|       type: i0.DriftSqlType.string, | ||||
|       requiredDuringInsert: true, | ||||
|       defaultConstraints: i0.GeneratedColumn.constraintIsAlways( | ||||
|           'REFERENCES local_album_entity (id) ON DELETE CASCADE')); | ||||
|   @override | ||||
|   List<i0.GeneratedColumn> get $columns => [assetId, albumId]; | ||||
|   @override | ||||
|   String get aliasedName => _alias ?? actualTableName; | ||||
|   @override | ||||
|   String get actualTableName => $name; | ||||
|   static const String $name = 'local_album_asset_entity'; | ||||
|   @override | ||||
|   i0.VerificationContext validateIntegrity( | ||||
|       i0.Insertable<i1.LocalAlbumAssetEntityData> instance, | ||||
|       {bool isInserting = false}) { | ||||
|     final context = i0.VerificationContext(); | ||||
|     final data = instance.toColumns(true); | ||||
|     if (data.containsKey('asset_id')) { | ||||
|       context.handle(_assetIdMeta, | ||||
|           assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta)); | ||||
|     } else if (isInserting) { | ||||
|       context.missing(_assetIdMeta); | ||||
|     } | ||||
|     if (data.containsKey('album_id')) { | ||||
|       context.handle(_albumIdMeta, | ||||
|           albumId.isAcceptableOrUnknown(data['album_id']!, _albumIdMeta)); | ||||
|     } else if (isInserting) { | ||||
|       context.missing(_albumIdMeta); | ||||
|     } | ||||
|     return context; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Set<i0.GeneratedColumn> get $primaryKey => {assetId, albumId}; | ||||
|   @override | ||||
|   i1.LocalAlbumAssetEntityData map(Map<String, dynamic> data, | ||||
|       {String? tablePrefix}) { | ||||
|     final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; | ||||
|     return i1.LocalAlbumAssetEntityData( | ||||
|       assetId: attachedDatabase.typeMapping | ||||
|           .read(i0.DriftSqlType.string, data['${effectivePrefix}asset_id'])!, | ||||
|       albumId: attachedDatabase.typeMapping | ||||
|           .read(i0.DriftSqlType.string, data['${effectivePrefix}album_id'])!, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   $LocalAlbumAssetEntityTable createAlias(String alias) { | ||||
|     return $LocalAlbumAssetEntityTable(attachedDatabase, alias); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   bool get withoutRowId => true; | ||||
|   @override | ||||
|   bool get isStrict => true; | ||||
| } | ||||
| 
 | ||||
| class LocalAlbumAssetEntityData extends i0.DataClass | ||||
|     implements i0.Insertable<i1.LocalAlbumAssetEntityData> { | ||||
|   final String assetId; | ||||
|   final String albumId; | ||||
|   const LocalAlbumAssetEntityData( | ||||
|       {required this.assetId, required this.albumId}); | ||||
|   @override | ||||
|   Map<String, i0.Expression> toColumns(bool nullToAbsent) { | ||||
|     final map = <String, i0.Expression>{}; | ||||
|     map['asset_id'] = i0.Variable<String>(assetId); | ||||
|     map['album_id'] = i0.Variable<String>(albumId); | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   factory LocalAlbumAssetEntityData.fromJson(Map<String, dynamic> json, | ||||
|       {i0.ValueSerializer? serializer}) { | ||||
|     serializer ??= i0.driftRuntimeOptions.defaultSerializer; | ||||
|     return LocalAlbumAssetEntityData( | ||||
|       assetId: serializer.fromJson<String>(json['assetId']), | ||||
|       albumId: serializer.fromJson<String>(json['albumId']), | ||||
|     ); | ||||
|   } | ||||
|   @override | ||||
|   Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) { | ||||
|     serializer ??= i0.driftRuntimeOptions.defaultSerializer; | ||||
|     return <String, dynamic>{ | ||||
|       'assetId': serializer.toJson<String>(assetId), | ||||
|       'albumId': serializer.toJson<String>(albumId), | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   i1.LocalAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => | ||||
|       i1.LocalAlbumAssetEntityData( | ||||
|         assetId: assetId ?? this.assetId, | ||||
|         albumId: albumId ?? this.albumId, | ||||
|       ); | ||||
|   LocalAlbumAssetEntityData copyWithCompanion( | ||||
|       i1.LocalAlbumAssetEntityCompanion data) { | ||||
|     return LocalAlbumAssetEntityData( | ||||
|       assetId: data.assetId.present ? data.assetId.value : this.assetId, | ||||
|       albumId: data.albumId.present ? data.albumId.value : this.albumId, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   String toString() { | ||||
|     return (StringBuffer('LocalAlbumAssetEntityData(') | ||||
|           ..write('assetId: $assetId, ') | ||||
|           ..write('albumId: $albumId') | ||||
|           ..write(')')) | ||||
|         .toString(); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => Object.hash(assetId, albumId); | ||||
|   @override | ||||
|   bool operator ==(Object other) => | ||||
|       identical(this, other) || | ||||
|       (other is i1.LocalAlbumAssetEntityData && | ||||
|           other.assetId == this.assetId && | ||||
|           other.albumId == this.albumId); | ||||
| } | ||||
| 
 | ||||
| class LocalAlbumAssetEntityCompanion | ||||
|     extends i0.UpdateCompanion<i1.LocalAlbumAssetEntityData> { | ||||
|   final i0.Value<String> assetId; | ||||
|   final i0.Value<String> albumId; | ||||
|   const LocalAlbumAssetEntityCompanion({ | ||||
|     this.assetId = const i0.Value.absent(), | ||||
|     this.albumId = const i0.Value.absent(), | ||||
|   }); | ||||
|   LocalAlbumAssetEntityCompanion.insert({ | ||||
|     required String assetId, | ||||
|     required String albumId, | ||||
|   })  : assetId = i0.Value(assetId), | ||||
|         albumId = i0.Value(albumId); | ||||
|   static i0.Insertable<i1.LocalAlbumAssetEntityData> custom({ | ||||
|     i0.Expression<String>? assetId, | ||||
|     i0.Expression<String>? albumId, | ||||
|   }) { | ||||
|     return i0.RawValuesInsertable({ | ||||
|       if (assetId != null) 'asset_id': assetId, | ||||
|       if (albumId != null) 'album_id': albumId, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   i1.LocalAlbumAssetEntityCompanion copyWith( | ||||
|       {i0.Value<String>? assetId, i0.Value<String>? albumId}) { | ||||
|     return i1.LocalAlbumAssetEntityCompanion( | ||||
|       assetId: assetId ?? this.assetId, | ||||
|       albumId: albumId ?? this.albumId, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Map<String, i0.Expression> toColumns(bool nullToAbsent) { | ||||
|     final map = <String, i0.Expression>{}; | ||||
|     if (assetId.present) { | ||||
|       map['asset_id'] = i0.Variable<String>(assetId.value); | ||||
|     } | ||||
|     if (albumId.present) { | ||||
|       map['album_id'] = i0.Variable<String>(albumId.value); | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   String toString() { | ||||
|     return (StringBuffer('LocalAlbumAssetEntityCompanion(') | ||||
|           ..write('assetId: $assetId, ') | ||||
|           ..write('albumId: $albumId') | ||||
|           ..write(')')) | ||||
|         .toString(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										17
									
								
								mobile/lib/infrastructure/entities/local_asset.entity.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								mobile/lib/infrastructure/entities/local_asset.entity.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import 'package:drift/drift.dart'; | ||||
| import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart'; | ||||
| import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; | ||||
|  | ||||
| @TableIndex(name: 'local_asset_checksum', columns: {#checksum}) | ||||
| class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin { | ||||
|   const LocalAssetEntity(); | ||||
|  | ||||
|   TextColumn get id => text()(); | ||||
|   TextColumn get checksum => text().nullable()(); | ||||
|  | ||||
|   // Only used during backup to mirror the favorite status of the asset in the server | ||||
|   BoolColumn get isFavorite => boolean().withDefault(const Constant(false))(); | ||||
|  | ||||
|   @override | ||||
|   Set<Column> get primaryKey => {id}; | ||||
| } | ||||
							
								
								
									
										658
									
								
								mobile/lib/infrastructure/entities/local_asset.entity.drift.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										658
									
								
								mobile/lib/infrastructure/entities/local_asset.entity.drift.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,658 @@ | ||||
| // dart format width=80 | ||||
| // ignore_for_file: type=lint | ||||
| import 'package:drift/drift.dart' as i0; | ||||
| import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' | ||||
|     as i1; | ||||
| import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as i2; | ||||
| import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart' | ||||
|     as i3; | ||||
| import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; | ||||
| 
 | ||||
| typedef $$LocalAssetEntityTableCreateCompanionBuilder | ||||
|     = i1.LocalAssetEntityCompanion Function({ | ||||
|   required String name, | ||||
|   required i2.AssetType type, | ||||
|   i0.Value<DateTime> createdAt, | ||||
|   i0.Value<DateTime> updatedAt, | ||||
|   i0.Value<int?> durationInSeconds, | ||||
|   required String id, | ||||
|   i0.Value<String?> checksum, | ||||
|   i0.Value<bool> isFavorite, | ||||
| }); | ||||
| typedef $$LocalAssetEntityTableUpdateCompanionBuilder | ||||
|     = i1.LocalAssetEntityCompanion Function({ | ||||
|   i0.Value<String> name, | ||||
|   i0.Value<i2.AssetType> type, | ||||
|   i0.Value<DateTime> createdAt, | ||||
|   i0.Value<DateTime> updatedAt, | ||||
|   i0.Value<int?> durationInSeconds, | ||||
|   i0.Value<String> id, | ||||
|   i0.Value<String?> checksum, | ||||
|   i0.Value<bool> isFavorite, | ||||
| }); | ||||
| 
 | ||||
| class $$LocalAssetEntityTableFilterComposer | ||||
|     extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAssetEntityTable> { | ||||
|   $$LocalAssetEntityTableFilterComposer({ | ||||
|     required super.$db, | ||||
|     required super.$table, | ||||
|     super.joinBuilder, | ||||
|     super.$addJoinBuilderToRootComposer, | ||||
|     super.$removeJoinBuilderFromRootComposer, | ||||
|   }); | ||||
|   i0.ColumnFilters<String> get name => $composableBuilder( | ||||
|       column: $table.name, builder: (column) => i0.ColumnFilters(column)); | ||||
| 
 | ||||
|   i0.ColumnWithTypeConverterFilters<i2.AssetType, i2.AssetType, int> get type => | ||||
|       $composableBuilder( | ||||
|           column: $table.type, | ||||
|           builder: (column) => i0.ColumnWithTypeConverterFilters(column)); | ||||
| 
 | ||||
|   i0.ColumnFilters<DateTime> get createdAt => $composableBuilder( | ||||
|       column: $table.createdAt, builder: (column) => i0.ColumnFilters(column)); | ||||
| 
 | ||||
|   i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder( | ||||
|       column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column)); | ||||
| 
 | ||||
|   i0.ColumnFilters<int> get durationInSeconds => $composableBuilder( | ||||
|       column: $table.durationInSeconds, | ||||
|       builder: (column) => i0.ColumnFilters(column)); | ||||
| 
 | ||||
|   i0.ColumnFilters<String> get id => $composableBuilder( | ||||
|       column: $table.id, builder: (column) => i0.ColumnFilters(column)); | ||||
| 
 | ||||
|   i0.ColumnFilters<String> get checksum => $composableBuilder( | ||||
|       column: $table.checksum, builder: (column) => i0.ColumnFilters(column)); | ||||
| 
 | ||||
|   i0.ColumnFilters<bool> get isFavorite => $composableBuilder( | ||||
|       column: $table.isFavorite, builder: (column) => i0.ColumnFilters(column)); | ||||
| } | ||||
| 
 | ||||
| class $$LocalAssetEntityTableOrderingComposer | ||||
|     extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAssetEntityTable> { | ||||
|   $$LocalAssetEntityTableOrderingComposer({ | ||||
|     required super.$db, | ||||
|     required super.$table, | ||||
|     super.joinBuilder, | ||||
|     super.$addJoinBuilderToRootComposer, | ||||
|     super.$removeJoinBuilderFromRootComposer, | ||||
|   }); | ||||
|   i0.ColumnOrderings<String> get name => $composableBuilder( | ||||
|       column: $table.name, builder: (column) => i0.ColumnOrderings(column)); | ||||
| 
 | ||||
|   i0.ColumnOrderings<int> get type => $composableBuilder( | ||||
|       column: $table.type, builder: (column) => i0.ColumnOrderings(column)); | ||||
| 
 | ||||
|   i0.ColumnOrderings<DateTime> get createdAt => $composableBuilder( | ||||
|       column: $table.createdAt, | ||||
|       builder: (column) => i0.ColumnOrderings(column)); | ||||
| 
 | ||||
|   i0.ColumnOrderings<DateTime> get updatedAt => $composableBuilder( | ||||
|       column: $table.updatedAt, | ||||
|       builder: (column) => i0.ColumnOrderings(column)); | ||||
| 
 | ||||
|   i0.ColumnOrderings<int> get durationInSeconds => $composableBuilder( | ||||
|       column: $table.durationInSeconds, | ||||
|       builder: (column) => i0.ColumnOrderings(column)); | ||||
| 
 | ||||
|   i0.ColumnOrderings<String> get id => $composableBuilder( | ||||
|       column: $table.id, builder: (column) => i0.ColumnOrderings(column)); | ||||
| 
 | ||||
|   i0.ColumnOrderings<String> get checksum => $composableBuilder( | ||||
|       column: $table.checksum, builder: (column) => i0.ColumnOrderings(column)); | ||||
| 
 | ||||
|   i0.ColumnOrderings<bool> get isFavorite => $composableBuilder( | ||||
|       column: $table.isFavorite, | ||||
|       builder: (column) => i0.ColumnOrderings(column)); | ||||
| } | ||||
| 
 | ||||
| class $$LocalAssetEntityTableAnnotationComposer | ||||
|     extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAssetEntityTable> { | ||||
|   $$LocalAssetEntityTableAnnotationComposer({ | ||||
|     required super.$db, | ||||
|     required super.$table, | ||||
|     super.joinBuilder, | ||||
|     super.$addJoinBuilderToRootComposer, | ||||
|     super.$removeJoinBuilderFromRootComposer, | ||||
|   }); | ||||
|   i0.GeneratedColumn<String> get name => | ||||
|       $composableBuilder(column: $table.name, builder: (column) => column); | ||||
| 
 | ||||
|   i0.GeneratedColumnWithTypeConverter<i2.AssetType, int> get type => | ||||
|       $composableBuilder(column: $table.type, builder: (column) => column); | ||||
| 
 | ||||
|   i0.GeneratedColumn<DateTime> get createdAt => | ||||
|       $composableBuilder(column: $table.createdAt, builder: (column) => column); | ||||
| 
 | ||||
|   i0.GeneratedColumn<DateTime> get updatedAt => | ||||
|       $composableBuilder(column: $table.updatedAt, builder: (column) => column); | ||||
| 
 | ||||
|   i0.GeneratedColumn<int> get durationInSeconds => $composableBuilder( | ||||
|       column: $table.durationInSeconds, builder: (column) => column); | ||||
| 
 | ||||
|   i0.GeneratedColumn<String> get id => | ||||
|       $composableBuilder(column: $table.id, builder: (column) => column); | ||||
| 
 | ||||
|   i0.GeneratedColumn<String> get checksum => | ||||
|       $composableBuilder(column: $table.checksum, builder: (column) => column); | ||||
| 
 | ||||
|   i0.GeneratedColumn<bool> get isFavorite => $composableBuilder( | ||||
|       column: $table.isFavorite, builder: (column) => column); | ||||
| } | ||||
| 
 | ||||
| class $$LocalAssetEntityTableTableManager extends i0.RootTableManager< | ||||
|     i0.GeneratedDatabase, | ||||
|     i1.$LocalAssetEntityTable, | ||||
|     i1.LocalAssetEntityData, | ||||
|     i1.$$LocalAssetEntityTableFilterComposer, | ||||
|     i1.$$LocalAssetEntityTableOrderingComposer, | ||||
|     i1.$$LocalAssetEntityTableAnnotationComposer, | ||||
|     $$LocalAssetEntityTableCreateCompanionBuilder, | ||||
|     $$LocalAssetEntityTableUpdateCompanionBuilder, | ||||
|     ( | ||||
|       i1.LocalAssetEntityData, | ||||
|       i0.BaseReferences<i0.GeneratedDatabase, i1.$LocalAssetEntityTable, | ||||
|           i1.LocalAssetEntityData> | ||||
|     ), | ||||
|     i1.LocalAssetEntityData, | ||||
|     i0.PrefetchHooks Function()> { | ||||
|   $$LocalAssetEntityTableTableManager( | ||||
|       i0.GeneratedDatabase db, i1.$LocalAssetEntityTable table) | ||||
|       : super(i0.TableManagerState( | ||||
|           db: db, | ||||
|           table: table, | ||||
|           createFilteringComposer: () => | ||||
|               i1.$$LocalAssetEntityTableFilterComposer($db: db, $table: table), | ||||
|           createOrderingComposer: () => i1 | ||||
|               .$$LocalAssetEntityTableOrderingComposer($db: db, $table: table), | ||||
|           createComputedFieldComposer: () => | ||||
|               i1.$$LocalAssetEntityTableAnnotationComposer( | ||||
|                   $db: db, $table: table), | ||||
|           updateCompanionCallback: ({ | ||||
|             i0.Value<String> name = const i0.Value.absent(), | ||||
|             i0.Value<i2.AssetType> type = const i0.Value.absent(), | ||||
|             i0.Value<DateTime> createdAt = const i0.Value.absent(), | ||||
|             i0.Value<DateTime> updatedAt = const i0.Value.absent(), | ||||
|             i0.Value<int?> durationInSeconds = const i0.Value.absent(), | ||||
|             i0.Value<String> id = const i0.Value.absent(), | ||||
|             i0.Value<String?> checksum = const i0.Value.absent(), | ||||
|             i0.Value<bool> isFavorite = const i0.Value.absent(), | ||||
|           }) => | ||||
|               i1.LocalAssetEntityCompanion( | ||||
|             name: name, | ||||
|             type: type, | ||||
|             createdAt: createdAt, | ||||
|             updatedAt: updatedAt, | ||||
|             durationInSeconds: durationInSeconds, | ||||
|             id: id, | ||||
|             checksum: checksum, | ||||
|             isFavorite: isFavorite, | ||||
|           ), | ||||
|           createCompanionCallback: ({ | ||||
|             required String name, | ||||
|             required i2.AssetType type, | ||||
|             i0.Value<DateTime> createdAt = const i0.Value.absent(), | ||||
|             i0.Value<DateTime> updatedAt = const i0.Value.absent(), | ||||
|             i0.Value<int?> durationInSeconds = const i0.Value.absent(), | ||||
|             required String id, | ||||
|             i0.Value<String?> checksum = const i0.Value.absent(), | ||||
|             i0.Value<bool> isFavorite = const i0.Value.absent(), | ||||
|           }) => | ||||
|               i1.LocalAssetEntityCompanion.insert( | ||||
|             name: name, | ||||
|             type: type, | ||||
|             createdAt: createdAt, | ||||
|             updatedAt: updatedAt, | ||||
|             durationInSeconds: durationInSeconds, | ||||
|             id: id, | ||||
|             checksum: checksum, | ||||
|             isFavorite: isFavorite, | ||||
|           ), | ||||
|           withReferenceMapper: (p0) => p0 | ||||
|               .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) | ||||
|               .toList(), | ||||
|           prefetchHooksCallback: null, | ||||
|         )); | ||||
| } | ||||
| 
 | ||||
| typedef $$LocalAssetEntityTableProcessedTableManager = i0.ProcessedTableManager< | ||||
|     i0.GeneratedDatabase, | ||||
|     i1.$LocalAssetEntityTable, | ||||
|     i1.LocalAssetEntityData, | ||||
|     i1.$$LocalAssetEntityTableFilterComposer, | ||||
|     i1.$$LocalAssetEntityTableOrderingComposer, | ||||
|     i1.$$LocalAssetEntityTableAnnotationComposer, | ||||
|     $$LocalAssetEntityTableCreateCompanionBuilder, | ||||
|     $$LocalAssetEntityTableUpdateCompanionBuilder, | ||||
|     ( | ||||
|       i1.LocalAssetEntityData, | ||||
|       i0.BaseReferences<i0.GeneratedDatabase, i1.$LocalAssetEntityTable, | ||||
|           i1.LocalAssetEntityData> | ||||
|     ), | ||||
|     i1.LocalAssetEntityData, | ||||
|     i0.PrefetchHooks Function()>; | ||||
| i0.Index get localAssetChecksum => i0.Index('local_asset_checksum', | ||||
|     'CREATE INDEX local_asset_checksum ON local_asset_entity (checksum)'); | ||||
| 
 | ||||
| class $LocalAssetEntityTable extends i3.LocalAssetEntity | ||||
|     with i0.TableInfo<$LocalAssetEntityTable, i1.LocalAssetEntityData> { | ||||
|   @override | ||||
|   final i0.GeneratedDatabase attachedDatabase; | ||||
|   final String? _alias; | ||||
|   $LocalAssetEntityTable(this.attachedDatabase, [this._alias]); | ||||
|   static const i0.VerificationMeta _nameMeta = | ||||
|       const i0.VerificationMeta('name'); | ||||
|   @override | ||||
|   late final i0.GeneratedColumn<String> name = i0.GeneratedColumn<String>( | ||||
|       'name', aliasedName, false, | ||||
|       type: i0.DriftSqlType.string, requiredDuringInsert: true); | ||||
|   @override | ||||
|   late final i0.GeneratedColumnWithTypeConverter<i2.AssetType, int> type = | ||||
|       i0.GeneratedColumn<int>('type', aliasedName, false, | ||||
|               type: i0.DriftSqlType.int, requiredDuringInsert: true) | ||||
|           .withConverter<i2.AssetType>( | ||||
|               i1.$LocalAssetEntityTable.$convertertype); | ||||
|   static const i0.VerificationMeta _createdAtMeta = | ||||
|       const i0.VerificationMeta('createdAt'); | ||||
|   @override | ||||
|   late final i0.GeneratedColumn<DateTime> createdAt = | ||||
|       i0.GeneratedColumn<DateTime>('created_at', aliasedName, false, | ||||
|           type: i0.DriftSqlType.dateTime, | ||||
|           requiredDuringInsert: false, | ||||
|           defaultValue: i4.currentDateAndTime); | ||||
|   static const i0.VerificationMeta _updatedAtMeta = | ||||
|       const i0.VerificationMeta('updatedAt'); | ||||
|   @override | ||||
|   late final i0.GeneratedColumn<DateTime> updatedAt = | ||||
|       i0.GeneratedColumn<DateTime>('updated_at', aliasedName, false, | ||||
|           type: i0.DriftSqlType.dateTime, | ||||
|           requiredDuringInsert: false, | ||||
|           defaultValue: i4.currentDateAndTime); | ||||
|   static const i0.VerificationMeta _durationInSecondsMeta = | ||||
|       const i0.VerificationMeta('durationInSeconds'); | ||||
|   @override | ||||
|   late final i0.GeneratedColumn<int> durationInSeconds = | ||||
|       i0.GeneratedColumn<int>('duration_in_seconds', aliasedName, true, | ||||
|           type: i0.DriftSqlType.int, requiredDuringInsert: false); | ||||
|   static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); | ||||
|   @override | ||||
|   late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>( | ||||
|       'id', aliasedName, false, | ||||
|       type: i0.DriftSqlType.string, requiredDuringInsert: true); | ||||
|   static const i0.VerificationMeta _checksumMeta = | ||||
|       const i0.VerificationMeta('checksum'); | ||||
|   @override | ||||
|   late final i0.GeneratedColumn<String> checksum = i0.GeneratedColumn<String>( | ||||
|       'checksum', aliasedName, true, | ||||
|       type: i0.DriftSqlType.string, requiredDuringInsert: false); | ||||
|   static const i0.VerificationMeta _isFavoriteMeta = | ||||
|       const i0.VerificationMeta('isFavorite'); | ||||
|   @override | ||||
|   late final i0.GeneratedColumn<bool> isFavorite = i0.GeneratedColumn<bool>( | ||||
|       'is_favorite', aliasedName, false, | ||||
|       type: i0.DriftSqlType.bool, | ||||
|       requiredDuringInsert: false, | ||||
|       defaultConstraints: i0.GeneratedColumn.constraintIsAlways( | ||||
|           'CHECK ("is_favorite" IN (0, 1))'), | ||||
|       defaultValue: const i4.Constant(false)); | ||||
|   @override | ||||
|   List<i0.GeneratedColumn> get $columns => [ | ||||
|         name, | ||||
|         type, | ||||
|         createdAt, | ||||
|         updatedAt, | ||||
|         durationInSeconds, | ||||
|         id, | ||||
|         checksum, | ||||
|         isFavorite | ||||
|       ]; | ||||
|   @override | ||||
|   String get aliasedName => _alias ?? actualTableName; | ||||
|   @override | ||||
|   String get actualTableName => $name; | ||||
|   static const String $name = 'local_asset_entity'; | ||||
|   @override | ||||
|   i0.VerificationContext validateIntegrity( | ||||
|       i0.Insertable<i1.LocalAssetEntityData> instance, | ||||
|       {bool isInserting = false}) { | ||||
|     final context = i0.VerificationContext(); | ||||
|     final data = instance.toColumns(true); | ||||
|     if (data.containsKey('name')) { | ||||
|       context.handle( | ||||
|           _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); | ||||
|     } else if (isInserting) { | ||||
|       context.missing(_nameMeta); | ||||
|     } | ||||
|     if (data.containsKey('created_at')) { | ||||
|       context.handle(_createdAtMeta, | ||||
|           createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); | ||||
|     } | ||||
|     if (data.containsKey('updated_at')) { | ||||
|       context.handle(_updatedAtMeta, | ||||
|           updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); | ||||
|     } | ||||
|     if (data.containsKey('duration_in_seconds')) { | ||||
|       context.handle( | ||||
|           _durationInSecondsMeta, | ||||
|           durationInSeconds.isAcceptableOrUnknown( | ||||
|               data['duration_in_seconds']!, _durationInSecondsMeta)); | ||||
|     } | ||||
|     if (data.containsKey('id')) { | ||||
|       context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); | ||||
|     } else if (isInserting) { | ||||
|       context.missing(_idMeta); | ||||
|     } | ||||
|     if (data.containsKey('checksum')) { | ||||
|       context.handle(_checksumMeta, | ||||
|           checksum.isAcceptableOrUnknown(data['checksum']!, _checksumMeta)); | ||||
|     } | ||||
|     if (data.containsKey('is_favorite')) { | ||||
|       context.handle( | ||||
|           _isFavoriteMeta, | ||||
|           isFavorite.isAcceptableOrUnknown( | ||||
|               data['is_favorite']!, _isFavoriteMeta)); | ||||
|     } | ||||
|     return context; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Set<i0.GeneratedColumn> get $primaryKey => {id}; | ||||
|   @override | ||||
|   i1.LocalAssetEntityData map(Map<String, dynamic> data, | ||||
|       {String? tablePrefix}) { | ||||
|     final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; | ||||
|     return i1.LocalAssetEntityData( | ||||
|       name: attachedDatabase.typeMapping | ||||
|           .read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!, | ||||
|       type: i1.$LocalAssetEntityTable.$convertertype.fromSql(attachedDatabase | ||||
|           .typeMapping | ||||
|           .read(i0.DriftSqlType.int, data['${effectivePrefix}type'])!), | ||||
|       createdAt: attachedDatabase.typeMapping.read( | ||||
|           i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, | ||||
|       updatedAt: attachedDatabase.typeMapping.read( | ||||
|           i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, | ||||
|       durationInSeconds: attachedDatabase.typeMapping.read( | ||||
|           i0.DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), | ||||
|       id: attachedDatabase.typeMapping | ||||
|           .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, | ||||
|       checksum: attachedDatabase.typeMapping | ||||
|           .read(i0.DriftSqlType.string, data['${effectivePrefix}checksum']), | ||||
|       isFavorite: attachedDatabase.typeMapping | ||||
|           .read(i0.DriftSqlType.bool, data['${effectivePrefix}is_favorite'])!, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   $LocalAssetEntityTable createAlias(String alias) { | ||||
|     return $LocalAssetEntityTable(attachedDatabase, alias); | ||||
|   } | ||||
| 
 | ||||
|   static i0.JsonTypeConverter2<i2.AssetType, int, int> $convertertype = | ||||
|       const i0.EnumIndexConverter<i2.AssetType>(i2.AssetType.values); | ||||
|   @override | ||||
|   bool get withoutRowId => true; | ||||
|   @override | ||||
|   bool get isStrict => true; | ||||
| } | ||||
| 
 | ||||
| class LocalAssetEntityData extends i0.DataClass | ||||
|     implements i0.Insertable<i1.LocalAssetEntityData> { | ||||
|   final String name; | ||||
|   final i2.AssetType type; | ||||
|   final DateTime createdAt; | ||||
|   final DateTime updatedAt; | ||||
|   final int? durationInSeconds; | ||||
|   final String id; | ||||
|   final String? checksum; | ||||
|   final bool isFavorite; | ||||
|   const LocalAssetEntityData( | ||||
|       {required this.name, | ||||
|       required this.type, | ||||
|       required this.createdAt, | ||||
|       required this.updatedAt, | ||||
|       this.durationInSeconds, | ||||
|       required this.id, | ||||
|       this.checksum, | ||||
|       required this.isFavorite}); | ||||
|   @override | ||||
|   Map<String, i0.Expression> toColumns(bool nullToAbsent) { | ||||
|     final map = <String, i0.Expression>{}; | ||||
|     map['name'] = i0.Variable<String>(name); | ||||
|     { | ||||
|       map['type'] = i0.Variable<int>( | ||||
|           i1.$LocalAssetEntityTable.$convertertype.toSql(type)); | ||||
|     } | ||||
|     map['created_at'] = i0.Variable<DateTime>(createdAt); | ||||
|     map['updated_at'] = i0.Variable<DateTime>(updatedAt); | ||||
|     if (!nullToAbsent || durationInSeconds != null) { | ||||
|       map['duration_in_seconds'] = i0.Variable<int>(durationInSeconds); | ||||
|     } | ||||
|     map['id'] = i0.Variable<String>(id); | ||||
|     if (!nullToAbsent || checksum != null) { | ||||
|       map['checksum'] = i0.Variable<String>(checksum); | ||||
|     } | ||||
|     map['is_favorite'] = i0.Variable<bool>(isFavorite); | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   factory LocalAssetEntityData.fromJson(Map<String, dynamic> json, | ||||
|       {i0.ValueSerializer? serializer}) { | ||||
|     serializer ??= i0.driftRuntimeOptions.defaultSerializer; | ||||
|     return LocalAssetEntityData( | ||||
|       name: serializer.fromJson<String>(json['name']), | ||||
|       type: i1.$LocalAssetEntityTable.$convertertype | ||||
|           .fromJson(serializer.fromJson<int>(json['type'])), | ||||
|       createdAt: serializer.fromJson<DateTime>(json['createdAt']), | ||||
|       updatedAt: serializer.fromJson<DateTime>(json['updatedAt']), | ||||
|       durationInSeconds: serializer.fromJson<int?>(json['durationInSeconds']), | ||||
|       id: serializer.fromJson<String>(json['id']), | ||||
|       checksum: serializer.fromJson<String?>(json['checksum']), | ||||
|       isFavorite: serializer.fromJson<bool>(json['isFavorite']), | ||||
|     ); | ||||
|   } | ||||
|   @override | ||||
|   Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) { | ||||
|     serializer ??= i0.driftRuntimeOptions.defaultSerializer; | ||||
|     return <String, dynamic>{ | ||||
|       'name': serializer.toJson<String>(name), | ||||
|       'type': serializer | ||||
|           .toJson<int>(i1.$LocalAssetEntityTable.$convertertype.toJson(type)), | ||||
|       'createdAt': serializer.toJson<DateTime>(createdAt), | ||||
|       'updatedAt': serializer.toJson<DateTime>(updatedAt), | ||||
|       'durationInSeconds': serializer.toJson<int?>(durationInSeconds), | ||||
|       'id': serializer.toJson<String>(id), | ||||
|       'checksum': serializer.toJson<String?>(checksum), | ||||
|       'isFavorite': serializer.toJson<bool>(isFavorite), | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   i1.LocalAssetEntityData copyWith( | ||||
|           {String? name, | ||||
|           i2.AssetType? type, | ||||
|           DateTime? createdAt, | ||||
|           DateTime? updatedAt, | ||||
|           i0.Value<int?> durationInSeconds = const i0.Value.absent(), | ||||
|           String? id, | ||||
|           i0.Value<String?> checksum = const i0.Value.absent(), | ||||
|           bool? isFavorite}) => | ||||
|       i1.LocalAssetEntityData( | ||||
|         name: name ?? this.name, | ||||
|         type: type ?? this.type, | ||||
|         createdAt: createdAt ?? this.createdAt, | ||||
|         updatedAt: updatedAt ?? this.updatedAt, | ||||
|         durationInSeconds: durationInSeconds.present | ||||
|             ? durationInSeconds.value | ||||
|             : this.durationInSeconds, | ||||
|         id: id ?? this.id, | ||||
|         checksum: checksum.present ? checksum.value : this.checksum, | ||||
|         isFavorite: isFavorite ?? this.isFavorite, | ||||
|       ); | ||||
|   LocalAssetEntityData copyWithCompanion(i1.LocalAssetEntityCompanion data) { | ||||
|     return LocalAssetEntityData( | ||||
|       name: data.name.present ? data.name.value : this.name, | ||||
|       type: data.type.present ? data.type.value : this.type, | ||||
|       createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, | ||||
|       updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, | ||||
|       durationInSeconds: data.durationInSeconds.present | ||||
|           ? data.durationInSeconds.value | ||||
|           : this.durationInSeconds, | ||||
|       id: data.id.present ? data.id.value : this.id, | ||||
|       checksum: data.checksum.present ? data.checksum.value : this.checksum, | ||||
|       isFavorite: | ||||
|           data.isFavorite.present ? data.isFavorite.value : this.isFavorite, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   String toString() { | ||||
|     return (StringBuffer('LocalAssetEntityData(') | ||||
|           ..write('name: $name, ') | ||||
|           ..write('type: $type, ') | ||||
|           ..write('createdAt: $createdAt, ') | ||||
|           ..write('updatedAt: $updatedAt, ') | ||||
|           ..write('durationInSeconds: $durationInSeconds, ') | ||||
|           ..write('id: $id, ') | ||||
|           ..write('checksum: $checksum, ') | ||||
|           ..write('isFavorite: $isFavorite') | ||||
|           ..write(')')) | ||||
|         .toString(); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => Object.hash(name, type, createdAt, updatedAt, | ||||
|       durationInSeconds, id, checksum, isFavorite); | ||||
|   @override | ||||
|   bool operator ==(Object other) => | ||||
|       identical(this, other) || | ||||
|       (other is i1.LocalAssetEntityData && | ||||
|           other.name == this.name && | ||||
|           other.type == this.type && | ||||
|           other.createdAt == this.createdAt && | ||||
|           other.updatedAt == this.updatedAt && | ||||
|           other.durationInSeconds == this.durationInSeconds && | ||||
|           other.id == this.id && | ||||
|           other.checksum == this.checksum && | ||||
|           other.isFavorite == this.isFavorite); | ||||
| } | ||||
| 
 | ||||
| class LocalAssetEntityCompanion | ||||
|     extends i0.UpdateCompanion<i1.LocalAssetEntityData> { | ||||
|   final i0.Value<String> name; | ||||
|   final i0.Value<i2.AssetType> type; | ||||
|   final i0.Value<DateTime> createdAt; | ||||
|   final i0.Value<DateTime> updatedAt; | ||||
|   final i0.Value<int?> durationInSeconds; | ||||
|   final i0.Value<String> id; | ||||
|   final i0.Value<String?> checksum; | ||||
|   final i0.Value<bool> isFavorite; | ||||
|   const LocalAssetEntityCompanion({ | ||||
|     this.name = const i0.Value.absent(), | ||||
|     this.type = const i0.Value.absent(), | ||||
|     this.createdAt = const i0.Value.absent(), | ||||
|     this.updatedAt = const i0.Value.absent(), | ||||
|     this.durationInSeconds = const i0.Value.absent(), | ||||
|     this.id = const i0.Value.absent(), | ||||
|     this.checksum = const i0.Value.absent(), | ||||
|     this.isFavorite = const i0.Value.absent(), | ||||
|   }); | ||||
|   LocalAssetEntityCompanion.insert({ | ||||
|     required String name, | ||||
|     required i2.AssetType type, | ||||
|     this.createdAt = const i0.Value.absent(), | ||||
|     this.updatedAt = const i0.Value.absent(), | ||||
|     this.durationInSeconds = const i0.Value.absent(), | ||||
|     required String id, | ||||
|     this.checksum = const i0.Value.absent(), | ||||
|     this.isFavorite = const i0.Value.absent(), | ||||
|   })  : name = i0.Value(name), | ||||
|         type = i0.Value(type), | ||||
|         id = i0.Value(id); | ||||
|   static i0.Insertable<i1.LocalAssetEntityData> custom({ | ||||
|     i0.Expression<String>? name, | ||||
|     i0.Expression<int>? type, | ||||
|     i0.Expression<DateTime>? createdAt, | ||||
|     i0.Expression<DateTime>? updatedAt, | ||||
|     i0.Expression<int>? durationInSeconds, | ||||
|     i0.Expression<String>? id, | ||||
|     i0.Expression<String>? checksum, | ||||
|     i0.Expression<bool>? isFavorite, | ||||
|   }) { | ||||
|     return i0.RawValuesInsertable({ | ||||
|       if (name != null) 'name': name, | ||||
|       if (type != null) 'type': type, | ||||
|       if (createdAt != null) 'created_at': createdAt, | ||||
|       if (updatedAt != null) 'updated_at': updatedAt, | ||||
|       if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, | ||||
|       if (id != null) 'id': id, | ||||
|       if (checksum != null) 'checksum': checksum, | ||||
|       if (isFavorite != null) 'is_favorite': isFavorite, | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   i1.LocalAssetEntityCompanion copyWith( | ||||
|       {i0.Value<String>? name, | ||||
|       i0.Value<i2.AssetType>? type, | ||||
|       i0.Value<DateTime>? createdAt, | ||||
|       i0.Value<DateTime>? updatedAt, | ||||
|       i0.Value<int?>? durationInSeconds, | ||||
|       i0.Value<String>? id, | ||||
|       i0.Value<String?>? checksum, | ||||
|       i0.Value<bool>? isFavorite}) { | ||||
|     return i1.LocalAssetEntityCompanion( | ||||
|       name: name ?? this.name, | ||||
|       type: type ?? this.type, | ||||
|       createdAt: createdAt ?? this.createdAt, | ||||
|       updatedAt: updatedAt ?? this.updatedAt, | ||||
|       durationInSeconds: durationInSeconds ?? this.durationInSeconds, | ||||
|       id: id ?? this.id, | ||||
|       checksum: checksum ?? this.checksum, | ||||
|       isFavorite: isFavorite ?? this.isFavorite, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Map<String, i0.Expression> toColumns(bool nullToAbsent) { | ||||
|     final map = <String, i0.Expression>{}; | ||||
|     if (name.present) { | ||||
|       map['name'] = i0.Variable<String>(name.value); | ||||
|     } | ||||
|     if (type.present) { | ||||
|       map['type'] = i0.Variable<int>( | ||||
|           i1.$LocalAssetEntityTable.$convertertype.toSql(type.value)); | ||||
|     } | ||||
|     if (createdAt.present) { | ||||
|       map['created_at'] = i0.Variable<DateTime>(createdAt.value); | ||||
|     } | ||||
|     if (updatedAt.present) { | ||||
|       map['updated_at'] = i0.Variable<DateTime>(updatedAt.value); | ||||
|     } | ||||
|     if (durationInSeconds.present) { | ||||
|       map['duration_in_seconds'] = i0.Variable<int>(durationInSeconds.value); | ||||
|     } | ||||
|     if (id.present) { | ||||
|       map['id'] = i0.Variable<String>(id.value); | ||||
|     } | ||||
|     if (checksum.present) { | ||||
|       map['checksum'] = i0.Variable<String>(checksum.value); | ||||
|     } | ||||
|     if (isFavorite.present) { | ||||
|       map['is_favorite'] = i0.Variable<bool>(isFavorite.value); | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   String toString() { | ||||
|     return (StringBuffer('LocalAssetEntityCompanion(') | ||||
|           ..write('name: $name, ') | ||||
|           ..write('type: $type, ') | ||||
|           ..write('createdAt: $createdAt, ') | ||||
|           ..write('updatedAt: $updatedAt, ') | ||||
|           ..write('durationInSeconds: $durationInSeconds, ') | ||||
|           ..write('id: $id, ') | ||||
|           ..write('checksum: $checksum, ') | ||||
|           ..write('isFavorite: $isFavorite') | ||||
|           ..write(')')) | ||||
|         .toString(); | ||||
|   } | ||||
| } | ||||
| @@ -3,6 +3,9 @@ import 'dart:async'; | ||||
| import 'package:drift/drift.dart'; | ||||
| import 'package:drift_flutter/drift_flutter.dart'; | ||||
| import 'package:immich_mobile/domain/interfaces/db.interface.dart'; | ||||
| import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; | ||||
| import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart'; | ||||
| import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; | ||||
| import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'; | ||||
| import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; | ||||
| import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'; | ||||
| @@ -25,7 +28,16 @@ class IsarDatabaseRepository implements IDatabaseRepository { | ||||
|       Zone.current[_kzoneTxn] == null ? _db.writeTxn(callback) : callback(); | ||||
| } | ||||
|  | ||||
| @DriftDatabase(tables: [UserEntity, UserMetadataEntity, PartnerEntity]) | ||||
| @DriftDatabase( | ||||
|   tables: [ | ||||
|     UserEntity, | ||||
|     UserMetadataEntity, | ||||
|     PartnerEntity, | ||||
|     LocalAlbumEntity, | ||||
|     LocalAssetEntity, | ||||
|     LocalAlbumAssetEntity, | ||||
|   ], | ||||
| ) | ||||
| class Drift extends $Drift implements IDatabaseRepository { | ||||
|   Drift([QueryExecutor? executor]) | ||||
|       : super( | ||||
| @@ -42,8 +54,9 @@ class Drift extends $Drift implements IDatabaseRepository { | ||||
|   @override | ||||
|   MigrationStrategy get migration => MigrationStrategy( | ||||
|         beforeOpen: (details) async { | ||||
|           await customStatement('PRAGMA journal_mode = WAL'); | ||||
|           await customStatement('PRAGMA foreign_keys = ON'); | ||||
|           await customStatement('PRAGMA synchronous = NORMAL'); | ||||
|           await customStatement('PRAGMA journal_mode = WAL'); | ||||
|         }, | ||||
|       ); | ||||
| } | ||||
|   | ||||
| @@ -7,6 +7,12 @@ import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift | ||||
|     as i2; | ||||
| import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart' | ||||
|     as i3; | ||||
| import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart' | ||||
|     as i4; | ||||
| import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart' | ||||
|     as i5; | ||||
| import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart' | ||||
|     as i6; | ||||
| 
 | ||||
| abstract class $Drift extends i0.GeneratedDatabase { | ||||
|   $Drift(i0.QueryExecutor e) : super(e); | ||||
| @@ -16,12 +22,25 @@ abstract class $Drift extends i0.GeneratedDatabase { | ||||
|       i2.$UserMetadataEntityTable(this); | ||||
|   late final i3.$PartnerEntityTable partnerEntity = | ||||
|       i3.$PartnerEntityTable(this); | ||||
|   late final i4.$LocalAlbumEntityTable localAlbumEntity = | ||||
|       i4.$LocalAlbumEntityTable(this); | ||||
|   late final i5.$LocalAssetEntityTable localAssetEntity = | ||||
|       i5.$LocalAssetEntityTable(this); | ||||
|   late final i6.$LocalAlbumAssetEntityTable localAlbumAssetEntity = | ||||
|       i6.$LocalAlbumAssetEntityTable(this); | ||||
|   @override | ||||
|   Iterable<i0.TableInfo<i0.Table, Object?>> get allTables => | ||||
|       allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>(); | ||||
|   @override | ||||
|   List<i0.DatabaseSchemaEntity> get allSchemaEntities => | ||||
|       [userEntity, userMetadataEntity, partnerEntity]; | ||||
|   List<i0.DatabaseSchemaEntity> get allSchemaEntities => [ | ||||
|         userEntity, | ||||
|         userMetadataEntity, | ||||
|         partnerEntity, | ||||
|         localAlbumEntity, | ||||
|         localAssetEntity, | ||||
|         localAlbumAssetEntity, | ||||
|         i5.localAssetChecksum | ||||
|       ]; | ||||
|   @override | ||||
|   i0.StreamQueryUpdateRules get streamUpdateRules => | ||||
|       const i0.StreamQueryUpdateRules( | ||||
| @@ -48,6 +67,22 @@ abstract class $Drift extends i0.GeneratedDatabase { | ||||
|               i0.TableUpdate('partner_entity', kind: i0.UpdateKind.delete), | ||||
|             ], | ||||
|           ), | ||||
|           i0.WritePropagation( | ||||
|             on: i0.TableUpdateQuery.onTableName('local_asset_entity', | ||||
|                 limitUpdateKind: i0.UpdateKind.delete), | ||||
|             result: [ | ||||
|               i0.TableUpdate('local_album_asset_entity', | ||||
|                   kind: i0.UpdateKind.delete), | ||||
|             ], | ||||
|           ), | ||||
|           i0.WritePropagation( | ||||
|             on: i0.TableUpdateQuery.onTableName('local_album_entity', | ||||
|                 limitUpdateKind: i0.UpdateKind.delete), | ||||
|             result: [ | ||||
|               i0.TableUpdate('local_album_asset_entity', | ||||
|                   kind: i0.UpdateKind.delete), | ||||
|             ], | ||||
|           ), | ||||
|         ], | ||||
|       ); | ||||
|   @override | ||||
| @@ -64,4 +99,10 @@ class $DriftManager { | ||||
|       i2.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity); | ||||
|   i3.$$PartnerEntityTableTableManager get partnerEntity => | ||||
|       i3.$$PartnerEntityTableTableManager(_db, _db.partnerEntity); | ||||
|   i4.$$LocalAlbumEntityTableTableManager get localAlbumEntity => | ||||
|       i4.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity); | ||||
|   i5.$$LocalAssetEntityTableTableManager get localAssetEntity => | ||||
|       i5.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity); | ||||
|   i6.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i6 | ||||
|       .$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity); | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,366 @@ | ||||
| import 'package:drift/drift.dart'; | ||||
| import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; | ||||
| import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; | ||||
| import 'package:immich_mobile/domain/models/local_album.model.dart'; | ||||
| import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; | ||||
| import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'; | ||||
| import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; | ||||
| import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; | ||||
| import 'package:platform/platform.dart'; | ||||
|  | ||||
| class DriftLocalAlbumRepository extends DriftDatabaseRepository | ||||
|     implements ILocalAlbumRepository { | ||||
|   final Drift _db; | ||||
|   final Platform _platform; | ||||
|   const DriftLocalAlbumRepository(this._db, {Platform? platform}) | ||||
|       : _platform = platform ?? const LocalPlatform(), | ||||
|         super(_db); | ||||
|  | ||||
|   @override | ||||
|   Future<List<LocalAlbum>> getAll({SortLocalAlbumsBy? sortBy}) { | ||||
|     final assetCount = _db.localAlbumAssetEntity.assetId.count(); | ||||
|  | ||||
|     final query = _db.localAlbumEntity.select().join([ | ||||
|       leftOuterJoin( | ||||
|         _db.localAlbumAssetEntity, | ||||
|         _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id), | ||||
|         useColumns: false, | ||||
|       ), | ||||
|     ]); | ||||
|     query | ||||
|       ..addColumns([assetCount]) | ||||
|       ..groupBy([_db.localAlbumEntity.id]); | ||||
|     if (sortBy == SortLocalAlbumsBy.id) { | ||||
|       query.orderBy([OrderingTerm.asc(_db.localAlbumEntity.id)]); | ||||
|     } | ||||
|     return query | ||||
|         .map( | ||||
|           (row) => row | ||||
|               .readTable(_db.localAlbumEntity) | ||||
|               .toDto(assetCount: row.read(assetCount) ?? 0), | ||||
|         ) | ||||
|         .get(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<void> delete(String albumId) => transaction(() async { | ||||
|         // Remove all assets that are only in this particular album | ||||
|         // We cannot remove all assets in the album because they might be in other albums in iOS | ||||
|         // That is not the case on Android since asset <-> album has one:one mapping | ||||
|         final assetsToDelete = _platform.isIOS | ||||
|             ? await _getUniqueAssetsInAlbum(albumId) | ||||
|             : await getAssetIdsForAlbum(albumId); | ||||
|         await _deleteAssets(assetsToDelete); | ||||
|  | ||||
|         // All the other assets that are still associated will be unlinked automatically on-cascade | ||||
|         await _db.managers.localAlbumEntity | ||||
|             .filter((a) => a.id.equals(albumId)) | ||||
|             .delete(); | ||||
|       }); | ||||
|  | ||||
|   @override | ||||
|   Future<void> syncAlbumDeletes( | ||||
|     String albumId, | ||||
|     Iterable<String> assetIdsToKeep, | ||||
|   ) async { | ||||
|     if (assetIdsToKeep.isEmpty) { | ||||
|       return Future.value(); | ||||
|     } | ||||
|  | ||||
|     final deleteSmt = _db.localAssetEntity.delete(); | ||||
|     deleteSmt.where((localAsset) { | ||||
|       final subQuery = _db.localAlbumAssetEntity.selectOnly() | ||||
|         ..addColumns([_db.localAlbumAssetEntity.assetId]) | ||||
|         ..join([ | ||||
|           innerJoin( | ||||
|             _db.localAlbumEntity, | ||||
|             _db.localAlbumAssetEntity.albumId | ||||
|                 .equalsExp(_db.localAlbumEntity.id), | ||||
|           ), | ||||
|         ]); | ||||
|       subQuery.where( | ||||
|         _db.localAlbumEntity.id.equals(albumId) & | ||||
|             _db.localAlbumAssetEntity.assetId.isNotIn(assetIdsToKeep), | ||||
|       ); | ||||
|       return localAsset.id.isInQuery(subQuery); | ||||
|     }); | ||||
|     await deleteSmt.go(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<void> upsert( | ||||
|     LocalAlbum localAlbum, { | ||||
|     Iterable<LocalAsset> toUpsert = const [], | ||||
|     Iterable<String> toDelete = const [], | ||||
|   }) { | ||||
|     final companion = LocalAlbumEntityCompanion.insert( | ||||
|       id: localAlbum.id, | ||||
|       name: localAlbum.name, | ||||
|       updatedAt: Value(localAlbum.updatedAt), | ||||
|       backupSelection: localAlbum.backupSelection, | ||||
|     ); | ||||
|  | ||||
|     return _db.transaction(() async { | ||||
|       await _db.localAlbumEntity | ||||
|           .insertOne(companion, onConflict: DoUpdate((_) => companion)); | ||||
|       await _addAssets(localAlbum.id, toUpsert); | ||||
|       await _removeAssets(localAlbum.id, toDelete); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<void> updateAll(Iterable<LocalAlbum> albums) { | ||||
|     return _db.transaction(() async { | ||||
|       await _db.localAlbumEntity | ||||
|           .update() | ||||
|           .write(const LocalAlbumEntityCompanion(marker_: Value(true))); | ||||
|  | ||||
|       await _db.batch((batch) { | ||||
|         for (final album in albums) { | ||||
|           final companion = LocalAlbumEntityCompanion.insert( | ||||
|             id: album.id, | ||||
|             name: album.name, | ||||
|             updatedAt: Value(album.updatedAt), | ||||
|             backupSelection: album.backupSelection, | ||||
|             marker_: const Value(null), | ||||
|           ); | ||||
|  | ||||
|           batch.insert( | ||||
|             _db.localAlbumEntity, | ||||
|             companion, | ||||
|             onConflict: DoUpdate((_) => companion), | ||||
|           ); | ||||
|         } | ||||
|       }); | ||||
|  | ||||
|       if (_platform.isAndroid) { | ||||
|         // On Android, an asset can only be in one album | ||||
|         // So, get the albums that are marked for deletion | ||||
|         // and delete all the assets that are in those albums | ||||
|         final deleteSmt = _db.localAssetEntity.delete(); | ||||
|         deleteSmt.where((localAsset) { | ||||
|           final subQuery = _db.localAlbumAssetEntity.selectOnly() | ||||
|             ..addColumns([_db.localAlbumAssetEntity.assetId]) | ||||
|             ..join([ | ||||
|               innerJoin( | ||||
|                 _db.localAlbumEntity, | ||||
|                 _db.localAlbumAssetEntity.albumId | ||||
|                     .equalsExp(_db.localAlbumEntity.id), | ||||
|               ), | ||||
|             ]); | ||||
|           subQuery.where(_db.localAlbumEntity.marker_.isNotNull()); | ||||
|           return localAsset.id.isInQuery(subQuery); | ||||
|         }); | ||||
|         await deleteSmt.go(); | ||||
|       } | ||||
|  | ||||
|       await _db.localAlbumEntity.deleteWhere((f) => f.marker_.isNotNull()); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<List<LocalAsset>> getAssetsForAlbum(String albumId) { | ||||
|     final query = _db.localAlbumAssetEntity.select().join( | ||||
|       [ | ||||
|         innerJoin( | ||||
|           _db.localAssetEntity, | ||||
|           _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), | ||||
|         ), | ||||
|       ], | ||||
|     ) | ||||
|       ..where(_db.localAlbumAssetEntity.albumId.equals(albumId)) | ||||
|       ..orderBy([OrderingTerm.asc(_db.localAssetEntity.id)]); | ||||
|     return query | ||||
|         .map((row) => row.readTable(_db.localAssetEntity).toDto()) | ||||
|         .get(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<List<String>> getAssetIdsForAlbum(String albumId) { | ||||
|     final query = _db.localAlbumAssetEntity.selectOnly() | ||||
|       ..addColumns([_db.localAlbumAssetEntity.assetId]) | ||||
|       ..where(_db.localAlbumAssetEntity.albumId.equals(albumId)); | ||||
|     return query | ||||
|         .map((row) => row.read(_db.localAlbumAssetEntity.assetId)!) | ||||
|         .get(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Future<void> processDelta({ | ||||
|     required List<LocalAsset> updates, | ||||
|     required List<String> deletes, | ||||
|     required Map<String, List<String>> assetAlbums, | ||||
|   }) { | ||||
|     return _db.transaction(() async { | ||||
|       await _deleteAssets(deletes); | ||||
|  | ||||
|       await _upsertAssets(updates); | ||||
|       // The ugly casting below is required for now because the generated code | ||||
|       // casts the returned values from the platform during decoding them | ||||
|       // and iterating over them causes the type to be List<Object?> instead of | ||||
|       // List<String> | ||||
|       await _db.batch((batch) async { | ||||
|         assetAlbums.cast<String, List<Object?>>().forEach((assetId, albumIds) { | ||||
|           batch.deleteWhere( | ||||
|             _db.localAlbumAssetEntity, | ||||
|             (f) => | ||||
|                 f.albumId.isNotIn(albumIds.cast<String?>().nonNulls) & | ||||
|                 f.assetId.equals(assetId), | ||||
|           ); | ||||
|         }); | ||||
|       }); | ||||
|       await _db.batch((batch) async { | ||||
|         assetAlbums.cast<String, List<Object?>>().forEach((assetId, albumIds) { | ||||
|           batch.insertAll( | ||||
|             _db.localAlbumAssetEntity, | ||||
|             albumIds.cast<String?>().nonNulls.map( | ||||
|                   (albumId) => LocalAlbumAssetEntityCompanion.insert( | ||||
|                     assetId: assetId, | ||||
|                     albumId: albumId, | ||||
|                   ), | ||||
|                 ), | ||||
|             onConflict: DoNothing(), | ||||
|           ); | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   Future<void> _addAssets(String albumId, Iterable<LocalAsset> assets) { | ||||
|     if (assets.isEmpty) { | ||||
|       return Future.value(); | ||||
|     } | ||||
|     return transaction(() async { | ||||
|       await _upsertAssets(assets); | ||||
|       await _db.localAlbumAssetEntity.insertAll( | ||||
|         assets.map( | ||||
|           (a) => LocalAlbumAssetEntityCompanion.insert( | ||||
|             assetId: a.id, | ||||
|             albumId: albumId, | ||||
|           ), | ||||
|         ), | ||||
|         mode: InsertMode.insertOrIgnore, | ||||
|       ); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   Future<void> _removeAssets(String albumId, Iterable<String> assetIds) async { | ||||
|     if (assetIds.isEmpty) { | ||||
|       return Future.value(); | ||||
|     } | ||||
|  | ||||
|     if (_platform.isAndroid) { | ||||
|       return _deleteAssets(assetIds); | ||||
|     } | ||||
|  | ||||
|     List<String> assetsToDelete = []; | ||||
|     List<String> assetsToUnLink = []; | ||||
|  | ||||
|     final uniqueAssets = await _getUniqueAssetsInAlbum(albumId); | ||||
|     if (uniqueAssets.isEmpty) { | ||||
|       assetsToUnLink = assetIds.toList(); | ||||
|     } else { | ||||
|       // Delete unique assets and unlink others | ||||
|       final uniqueSet = uniqueAssets.toSet(); | ||||
|  | ||||
|       for (final assetId in assetIds) { | ||||
|         if (uniqueSet.contains(assetId)) { | ||||
|           assetsToDelete.add(assetId); | ||||
|         } else { | ||||
|           assetsToUnLink.add(assetId); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return transaction(() async { | ||||
|       if (assetsToUnLink.isNotEmpty) { | ||||
|         await _db.batch( | ||||
|           (batch) => batch.deleteWhere( | ||||
|             _db.localAlbumAssetEntity, | ||||
|             (f) => f.assetId.isIn(assetsToUnLink) & f.albumId.equals(albumId), | ||||
|           ), | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       await _deleteAssets(assetsToDelete); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   /// Get all asset ids that are only in this album and not in other albums. | ||||
|   /// This is useful in cases where the album is a smart album or a user-created album, especially on iOS | ||||
|   Future<List<String>> _getUniqueAssetsInAlbum(String albumId) { | ||||
|     final assetId = _db.localAlbumAssetEntity.assetId; | ||||
|     final query = _db.localAlbumAssetEntity.selectOnly() | ||||
|       ..addColumns([assetId]) | ||||
|       ..groupBy( | ||||
|         [assetId], | ||||
|         having: _db.localAlbumAssetEntity.albumId.count().equals(1) & | ||||
|             _db.localAlbumAssetEntity.albumId.equals(albumId), | ||||
|       ); | ||||
|  | ||||
|     return query.map((row) => row.read(assetId)!).get(); | ||||
|   } | ||||
|  | ||||
|   Future<void> _upsertAssets(Iterable<LocalAsset> localAssets) { | ||||
|     if (localAssets.isEmpty) { | ||||
|       return Future.value(); | ||||
|     } | ||||
|  | ||||
|     return _db.batch((batch) async { | ||||
|       batch.insertAllOnConflictUpdate( | ||||
|         _db.localAssetEntity, | ||||
|         localAssets.map( | ||||
|           (a) => LocalAssetEntityCompanion.insert( | ||||
|             name: a.name, | ||||
|             type: a.type, | ||||
|             createdAt: Value(a.createdAt), | ||||
|             updatedAt: Value(a.updatedAt), | ||||
|             durationInSeconds: Value.absentIfNull(a.durationInSeconds), | ||||
|             id: a.id, | ||||
|             checksum: Value.absentIfNull(a.checksum), | ||||
|           ), | ||||
|         ), | ||||
|       ); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   Future<void> _deleteAssets(Iterable<String> ids) { | ||||
|     if (ids.isEmpty) { | ||||
|       return Future.value(); | ||||
|     } | ||||
|  | ||||
|     return _db.batch( | ||||
|       (batch) => batch.deleteWhere( | ||||
|         _db.localAssetEntity, | ||||
|         (f) => f.id.isIn(ids), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| extension on LocalAlbumEntityData { | ||||
|   LocalAlbum toDto({int assetCount = 0}) { | ||||
|     return LocalAlbum( | ||||
|       id: id, | ||||
|       name: name, | ||||
|       updatedAt: updatedAt, | ||||
|       assetCount: assetCount, | ||||
|       backupSelection: backupSelection, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| extension on LocalAssetEntityData { | ||||
|   LocalAsset toDto() { | ||||
|     return LocalAsset( | ||||
|       id: id, | ||||
|       name: name, | ||||
|       checksum: checksum, | ||||
|       type: type, | ||||
|       createdAt: createdAt, | ||||
|       updatedAt: updatedAt, | ||||
|       durationInSeconds: durationInSeconds, | ||||
|       isFavorite: isFavorite, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										10
									
								
								mobile/lib/infrastructure/utils/asset.mixin.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								mobile/lib/infrastructure/utils/asset.mixin.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import 'package:drift/drift.dart'; | ||||
| import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; | ||||
|  | ||||
| mixin AssetEntityMixin on Table { | ||||
|   TextColumn get name => text()(); | ||||
|   IntColumn get type => intEnum<AssetType>()(); | ||||
|   DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); | ||||
|   DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); | ||||
|   IntColumn get durationInSeconds => integer().nullable()(); | ||||
| } | ||||
							
								
								
									
										501
									
								
								mobile/lib/platform/native_sync_api.g.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										501
									
								
								mobile/lib/platform/native_sync_api.g.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,501 @@ | ||||
| // Autogenerated from Pigeon (v25.3.2), do not edit directly. | ||||
| // See also: https://pub.dev/packages/pigeon | ||||
| // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers | ||||
| 
 | ||||
| import 'dart:async'; | ||||
| import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; | ||||
| 
 | ||||
| import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; | ||||
| import 'package:flutter/services.dart'; | ||||
| 
 | ||||
| PlatformException _createConnectionError(String channelName) { | ||||
|   return PlatformException( | ||||
|     code: 'channel-error', | ||||
|     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 && | ||||
|         a.indexed | ||||
|             .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) => | ||||
|             (b as Map<Object?, Object?>).containsKey(entry.key) && | ||||
|             _deepEquals(entry.value, b[entry.key])); | ||||
|   } | ||||
|   return a == b; | ||||
| } | ||||
| 
 | ||||
| class PlatformAsset { | ||||
|   PlatformAsset({ | ||||
|     required this.id, | ||||
|     required this.name, | ||||
|     required this.type, | ||||
|     this.createdAt, | ||||
|     this.updatedAt, | ||||
|     required this.durationInSeconds, | ||||
|   }); | ||||
| 
 | ||||
|   String id; | ||||
| 
 | ||||
|   String name; | ||||
| 
 | ||||
|   int type; | ||||
| 
 | ||||
|   int? createdAt; | ||||
| 
 | ||||
|   int? updatedAt; | ||||
| 
 | ||||
|   int durationInSeconds; | ||||
| 
 | ||||
|   List<Object?> _toList() { | ||||
|     return <Object?>[ | ||||
|       id, | ||||
|       name, | ||||
|       type, | ||||
|       createdAt, | ||||
|       updatedAt, | ||||
|       durationInSeconds, | ||||
|     ]; | ||||
|   } | ||||
| 
 | ||||
|   Object encode() { | ||||
|     return _toList(); | ||||
|   } | ||||
| 
 | ||||
|   static PlatformAsset decode(Object result) { | ||||
|     result as List<Object?>; | ||||
|     return PlatformAsset( | ||||
|       id: result[0]! as String, | ||||
|       name: result[1]! as String, | ||||
|       type: result[2]! as int, | ||||
|       createdAt: result[3] as int?, | ||||
|       updatedAt: result[4] as int?, | ||||
|       durationInSeconds: result[5]! as int, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   // ignore: avoid_equals_and_hash_code_on_mutable_classes | ||||
|   bool operator ==(Object other) { | ||||
|     if (other is! PlatformAsset || other.runtimeType != runtimeType) { | ||||
|       return false; | ||||
|     } | ||||
|     if (identical(this, other)) { | ||||
|       return true; | ||||
|     } | ||||
|     return _deepEquals(encode(), other.encode()); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   // ignore: avoid_equals_and_hash_code_on_mutable_classes | ||||
|   int get hashCode => Object.hashAll(_toList()); | ||||
| } | ||||
| 
 | ||||
| class PlatformAlbum { | ||||
|   PlatformAlbum({ | ||||
|     required this.id, | ||||
|     required this.name, | ||||
|     this.updatedAt, | ||||
|     required this.isCloud, | ||||
|     required this.assetCount, | ||||
|   }); | ||||
| 
 | ||||
|   String id; | ||||
| 
 | ||||
|   String name; | ||||
| 
 | ||||
|   int? updatedAt; | ||||
| 
 | ||||
|   bool isCloud; | ||||
| 
 | ||||
|   int assetCount; | ||||
| 
 | ||||
|   List<Object?> _toList() { | ||||
|     return <Object?>[ | ||||
|       id, | ||||
|       name, | ||||
|       updatedAt, | ||||
|       isCloud, | ||||
|       assetCount, | ||||
|     ]; | ||||
|   } | ||||
| 
 | ||||
|   Object encode() { | ||||
|     return _toList(); | ||||
|   } | ||||
| 
 | ||||
|   static PlatformAlbum decode(Object result) { | ||||
|     result as List<Object?>; | ||||
|     return PlatformAlbum( | ||||
|       id: result[0]! as String, | ||||
|       name: result[1]! as String, | ||||
|       updatedAt: result[2] as int?, | ||||
|       isCloud: result[3]! as bool, | ||||
|       assetCount: result[4]! as int, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   // ignore: avoid_equals_and_hash_code_on_mutable_classes | ||||
|   bool operator ==(Object other) { | ||||
|     if (other is! PlatformAlbum || other.runtimeType != runtimeType) { | ||||
|       return false; | ||||
|     } | ||||
|     if (identical(this, other)) { | ||||
|       return true; | ||||
|     } | ||||
|     return _deepEquals(encode(), other.encode()); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   // ignore: avoid_equals_and_hash_code_on_mutable_classes | ||||
|   int get hashCode => Object.hashAll(_toList()); | ||||
| } | ||||
| 
 | ||||
| class SyncDelta { | ||||
|   SyncDelta({ | ||||
|     required this.hasChanges, | ||||
|     required this.updates, | ||||
|     required this.deletes, | ||||
|     required this.assetAlbums, | ||||
|   }); | ||||
| 
 | ||||
|   bool hasChanges; | ||||
| 
 | ||||
|   List<PlatformAsset> updates; | ||||
| 
 | ||||
|   List<String> deletes; | ||||
| 
 | ||||
|   Map<String, List<String>> assetAlbums; | ||||
| 
 | ||||
|   List<Object?> _toList() { | ||||
|     return <Object?>[ | ||||
|       hasChanges, | ||||
|       updates, | ||||
|       deletes, | ||||
|       assetAlbums, | ||||
|     ]; | ||||
|   } | ||||
| 
 | ||||
|   Object encode() { | ||||
|     return _toList(); | ||||
|   } | ||||
| 
 | ||||
|   static SyncDelta decode(Object result) { | ||||
|     result as List<Object?>; | ||||
|     return SyncDelta( | ||||
|       hasChanges: result[0]! as bool, | ||||
|       updates: (result[1] as List<Object?>?)!.cast<PlatformAsset>(), | ||||
|       deletes: (result[2] as List<Object?>?)!.cast<String>(), | ||||
|       assetAlbums: | ||||
|           (result[3] as Map<Object?, Object?>?)!.cast<String, List<String>>(), | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   // ignore: avoid_equals_and_hash_code_on_mutable_classes | ||||
|   bool operator ==(Object other) { | ||||
|     if (other is! SyncDelta || other.runtimeType != runtimeType) { | ||||
|       return false; | ||||
|     } | ||||
|     if (identical(this, other)) { | ||||
|       return true; | ||||
|     } | ||||
|     return _deepEquals(encode(), other.encode()); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   // ignore: avoid_equals_and_hash_code_on_mutable_classes | ||||
|   int get hashCode => Object.hashAll(_toList()); | ||||
| } | ||||
| 
 | ||||
| class _PigeonCodec extends StandardMessageCodec { | ||||
|   const _PigeonCodec(); | ||||
|   @override | ||||
|   void writeValue(WriteBuffer buffer, Object? value) { | ||||
|     if (value is int) { | ||||
|       buffer.putUint8(4); | ||||
|       buffer.putInt64(value); | ||||
|     } else if (value is PlatformAsset) { | ||||
|       buffer.putUint8(129); | ||||
|       writeValue(buffer, value.encode()); | ||||
|     } else if (value is PlatformAlbum) { | ||||
|       buffer.putUint8(130); | ||||
|       writeValue(buffer, value.encode()); | ||||
|     } else if (value is SyncDelta) { | ||||
|       buffer.putUint8(131); | ||||
|       writeValue(buffer, value.encode()); | ||||
|     } else { | ||||
|       super.writeValue(buffer, value); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Object? readValueOfType(int type, ReadBuffer buffer) { | ||||
|     switch (type) { | ||||
|       case 129: | ||||
|         return PlatformAsset.decode(readValue(buffer)!); | ||||
|       case 130: | ||||
|         return PlatformAlbum.decode(readValue(buffer)!); | ||||
|       case 131: | ||||
|         return SyncDelta.decode(readValue(buffer)!); | ||||
|       default: | ||||
|         return super.readValueOfType(type, buffer); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class NativeSyncApi { | ||||
|   /// Constructor for [NativeSyncApi].  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. | ||||
|   NativeSyncApi( | ||||
|       {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) | ||||
|       : pigeonVar_binaryMessenger = binaryMessenger, | ||||
|         pigeonVar_messageChannelSuffix = | ||||
|             messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; | ||||
|   final BinaryMessenger? pigeonVar_binaryMessenger; | ||||
| 
 | ||||
|   static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec(); | ||||
| 
 | ||||
|   final String pigeonVar_messageChannelSuffix; | ||||
| 
 | ||||
|   Future<bool> shouldFullSync() async { | ||||
|     final String pigeonVar_channelName = | ||||
|         'dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$pigeonVar_messageChannelSuffix'; | ||||
|     final BasicMessageChannel<Object?> pigeonVar_channel = | ||||
|         BasicMessageChannel<Object?>( | ||||
|       pigeonVar_channelName, | ||||
|       pigeonChannelCodec, | ||||
|       binaryMessenger: pigeonVar_binaryMessenger, | ||||
|     ); | ||||
|     final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null); | ||||
|     final List<Object?>? pigeonVar_replyList = | ||||
|         await pigeonVar_sendFuture as List<Object?>?; | ||||
|     if (pigeonVar_replyList == null) { | ||||
|       throw _createConnectionError(pigeonVar_channelName); | ||||
|     } else if (pigeonVar_replyList.length > 1) { | ||||
|       throw PlatformException( | ||||
|         code: pigeonVar_replyList[0]! as String, | ||||
|         message: pigeonVar_replyList[1] as String?, | ||||
|         details: pigeonVar_replyList[2], | ||||
|       ); | ||||
|     } else if (pigeonVar_replyList[0] == null) { | ||||
|       throw PlatformException( | ||||
|         code: 'null-error', | ||||
|         message: 'Host platform returned null value for non-null return value.', | ||||
|       ); | ||||
|     } else { | ||||
|       return (pigeonVar_replyList[0] as bool?)!; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Future<SyncDelta> getMediaChanges() async { | ||||
|     final String pigeonVar_channelName = | ||||
|         'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$pigeonVar_messageChannelSuffix'; | ||||
|     final BasicMessageChannel<Object?> pigeonVar_channel = | ||||
|         BasicMessageChannel<Object?>( | ||||
|       pigeonVar_channelName, | ||||
|       pigeonChannelCodec, | ||||
|       binaryMessenger: pigeonVar_binaryMessenger, | ||||
|     ); | ||||
|     final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null); | ||||
|     final List<Object?>? pigeonVar_replyList = | ||||
|         await pigeonVar_sendFuture as List<Object?>?; | ||||
|     if (pigeonVar_replyList == null) { | ||||
|       throw _createConnectionError(pigeonVar_channelName); | ||||
|     } else if (pigeonVar_replyList.length > 1) { | ||||
|       throw PlatformException( | ||||
|         code: pigeonVar_replyList[0]! as String, | ||||
|         message: pigeonVar_replyList[1] as String?, | ||||
|         details: pigeonVar_replyList[2], | ||||
|       ); | ||||
|     } else if (pigeonVar_replyList[0] == null) { | ||||
|       throw PlatformException( | ||||
|         code: 'null-error', | ||||
|         message: 'Host platform returned null value for non-null return value.', | ||||
|       ); | ||||
|     } else { | ||||
|       return (pigeonVar_replyList[0] as SyncDelta?)!; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Future<void> checkpointSync() async { | ||||
|     final String pigeonVar_channelName = | ||||
|         'dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync$pigeonVar_messageChannelSuffix'; | ||||
|     final BasicMessageChannel<Object?> pigeonVar_channel = | ||||
|         BasicMessageChannel<Object?>( | ||||
|       pigeonVar_channelName, | ||||
|       pigeonChannelCodec, | ||||
|       binaryMessenger: pigeonVar_binaryMessenger, | ||||
|     ); | ||||
|     final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null); | ||||
|     final List<Object?>? pigeonVar_replyList = | ||||
|         await pigeonVar_sendFuture as List<Object?>?; | ||||
|     if (pigeonVar_replyList == null) { | ||||
|       throw _createConnectionError(pigeonVar_channelName); | ||||
|     } else if (pigeonVar_replyList.length > 1) { | ||||
|       throw PlatformException( | ||||
|         code: pigeonVar_replyList[0]! as String, | ||||
|         message: pigeonVar_replyList[1] as String?, | ||||
|         details: pigeonVar_replyList[2], | ||||
|       ); | ||||
|     } else { | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Future<void> clearSyncCheckpoint() async { | ||||
|     final String pigeonVar_channelName = | ||||
|         'dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint$pigeonVar_messageChannelSuffix'; | ||||
|     final BasicMessageChannel<Object?> pigeonVar_channel = | ||||
|         BasicMessageChannel<Object?>( | ||||
|       pigeonVar_channelName, | ||||
|       pigeonChannelCodec, | ||||
|       binaryMessenger: pigeonVar_binaryMessenger, | ||||
|     ); | ||||
|     final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null); | ||||
|     final List<Object?>? pigeonVar_replyList = | ||||
|         await pigeonVar_sendFuture as List<Object?>?; | ||||
|     if (pigeonVar_replyList == null) { | ||||
|       throw _createConnectionError(pigeonVar_channelName); | ||||
|     } else if (pigeonVar_replyList.length > 1) { | ||||
|       throw PlatformException( | ||||
|         code: pigeonVar_replyList[0]! as String, | ||||
|         message: pigeonVar_replyList[1] as String?, | ||||
|         details: pigeonVar_replyList[2], | ||||
|       ); | ||||
|     } else { | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Future<List<String>> getAssetIdsForAlbum(String albumId) async { | ||||
|     final String pigeonVar_channelName = | ||||
|         'dev.flutter.pigeon.immich_mobile.NativeSyncApi.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 List<Object?>? pigeonVar_replyList = | ||||
|         await pigeonVar_sendFuture as List<Object?>?; | ||||
|     if (pigeonVar_replyList == null) { | ||||
|       throw _createConnectionError(pigeonVar_channelName); | ||||
|     } else if (pigeonVar_replyList.length > 1) { | ||||
|       throw PlatformException( | ||||
|         code: pigeonVar_replyList[0]! as String, | ||||
|         message: pigeonVar_replyList[1] as String?, | ||||
|         details: pigeonVar_replyList[2], | ||||
|       ); | ||||
|     } else if (pigeonVar_replyList[0] == null) { | ||||
|       throw PlatformException( | ||||
|         code: 'null-error', | ||||
|         message: 'Host platform returned null value for non-null return value.', | ||||
|       ); | ||||
|     } else { | ||||
|       return (pigeonVar_replyList[0] as List<Object?>?)!.cast<String>(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Future<List<PlatformAlbum>> getAlbums() async { | ||||
|     final String pigeonVar_channelName = | ||||
|         'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$pigeonVar_messageChannelSuffix'; | ||||
|     final BasicMessageChannel<Object?> pigeonVar_channel = | ||||
|         BasicMessageChannel<Object?>( | ||||
|       pigeonVar_channelName, | ||||
|       pigeonChannelCodec, | ||||
|       binaryMessenger: pigeonVar_binaryMessenger, | ||||
|     ); | ||||
|     final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null); | ||||
|     final List<Object?>? pigeonVar_replyList = | ||||
|         await pigeonVar_sendFuture as List<Object?>?; | ||||
|     if (pigeonVar_replyList == null) { | ||||
|       throw _createConnectionError(pigeonVar_channelName); | ||||
|     } else if (pigeonVar_replyList.length > 1) { | ||||
|       throw PlatformException( | ||||
|         code: pigeonVar_replyList[0]! as String, | ||||
|         message: pigeonVar_replyList[1] as String?, | ||||
|         details: pigeonVar_replyList[2], | ||||
|       ); | ||||
|     } else if (pigeonVar_replyList[0] == null) { | ||||
|       throw PlatformException( | ||||
|         code: 'null-error', | ||||
|         message: 'Host platform returned null value for non-null return value.', | ||||
|       ); | ||||
|     } else { | ||||
|       return (pigeonVar_replyList[0] as List<Object?>?)!.cast<PlatformAlbum>(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Future<int> getAssetsCountSince(String albumId, int timestamp) async { | ||||
|     final String pigeonVar_channelName = | ||||
|         'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince$pigeonVar_messageChannelSuffix'; | ||||
|     final BasicMessageChannel<Object?> pigeonVar_channel = | ||||
|         BasicMessageChannel<Object?>( | ||||
|       pigeonVar_channelName, | ||||
|       pigeonChannelCodec, | ||||
|       binaryMessenger: pigeonVar_binaryMessenger, | ||||
|     ); | ||||
|     final Future<Object?> pigeonVar_sendFuture = | ||||
|         pigeonVar_channel.send(<Object?>[albumId, timestamp]); | ||||
|     final List<Object?>? pigeonVar_replyList = | ||||
|         await pigeonVar_sendFuture as List<Object?>?; | ||||
|     if (pigeonVar_replyList == null) { | ||||
|       throw _createConnectionError(pigeonVar_channelName); | ||||
|     } else if (pigeonVar_replyList.length > 1) { | ||||
|       throw PlatformException( | ||||
|         code: pigeonVar_replyList[0]! as String, | ||||
|         message: pigeonVar_replyList[1] as String?, | ||||
|         details: pigeonVar_replyList[2], | ||||
|       ); | ||||
|     } else if (pigeonVar_replyList[0] == null) { | ||||
|       throw PlatformException( | ||||
|         code: 'null-error', | ||||
|         message: 'Host platform returned null value for non-null return value.', | ||||
|       ); | ||||
|     } else { | ||||
|       return (pigeonVar_replyList[0] as int?)!; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   Future<List<PlatformAsset>> getAssetsForAlbum(String albumId, | ||||
|       {int? updatedTimeCond}) async { | ||||
|     final String pigeonVar_channelName = | ||||
|         'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$pigeonVar_messageChannelSuffix'; | ||||
|     final BasicMessageChannel<Object?> pigeonVar_channel = | ||||
|         BasicMessageChannel<Object?>( | ||||
|       pigeonVar_channelName, | ||||
|       pigeonChannelCodec, | ||||
|       binaryMessenger: pigeonVar_binaryMessenger, | ||||
|     ); | ||||
|     final Future<Object?> pigeonVar_sendFuture = | ||||
|         pigeonVar_channel.send(<Object?>[albumId, updatedTimeCond]); | ||||
|     final List<Object?>? pigeonVar_replyList = | ||||
|         await pigeonVar_sendFuture as List<Object?>?; | ||||
|     if (pigeonVar_replyList == null) { | ||||
|       throw _createConnectionError(pigeonVar_channelName); | ||||
|     } else if (pigeonVar_replyList.length > 1) { | ||||
|       throw PlatformException( | ||||
|         code: pigeonVar_replyList[0]! as String, | ||||
|         message: pigeonVar_replyList[1] as String?, | ||||
|         details: pigeonVar_replyList[2], | ||||
|       ); | ||||
|     } else if (pigeonVar_replyList[0] == null) { | ||||
|       throw PlatformException( | ||||
|         code: 'null-error', | ||||
|         message: 'Host platform returned null value for non-null return value.', | ||||
|       ); | ||||
|     } else { | ||||
|       return (pigeonVar_replyList[0] as List<Object?>?)!.cast<PlatformAsset>(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										68
									
								
								mobile/lib/presentation/pages/dev/dev_logger.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								mobile/lib/presentation/pages/dev/dev_logger.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| import 'dart:async'; | ||||
|  | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:immich_mobile/domain/models/log.model.dart'; | ||||
| import 'package:immich_mobile/infrastructure/entities/log.entity.dart'; | ||||
| import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; | ||||
| // ignore: import_rule_isar | ||||
| import 'package:isar/isar.dart'; | ||||
|  | ||||
| const kDevLoggerTag = 'DEV'; | ||||
|  | ||||
| abstract final class DLog { | ||||
|   const DLog(); | ||||
|  | ||||
|   static Stream<List<LogMessage>> watchLog() { | ||||
|     final db = Isar.getInstance(); | ||||
|     if (db == null) { | ||||
|       debugPrint('Isar is not initialized'); | ||||
|       return const Stream.empty(); | ||||
|     } | ||||
|  | ||||
|     return db.loggerMessages | ||||
|         .filter() | ||||
|         .context1EqualTo(kDevLoggerTag) | ||||
|         .sortByCreatedAtDesc() | ||||
|         .watch(fireImmediately: true) | ||||
|         .map((logs) => logs.map((log) => log.toDto()).toList()); | ||||
|   } | ||||
|  | ||||
|   static void clearLog() { | ||||
|     final db = Isar.getInstance(); | ||||
|     if (db == null) { | ||||
|       debugPrint('Isar is not initialized'); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     db.writeTxnSync(() { | ||||
|       db.loggerMessages.filter().context1EqualTo(kDevLoggerTag).deleteAllSync(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   static void log(String message, [Object? error, StackTrace? stackTrace]) { | ||||
|     debugPrint('[$kDevLoggerTag] [${DateTime.now()}] $message'); | ||||
|     if (error != null) { | ||||
|       debugPrint('Error: $error'); | ||||
|     } | ||||
|     if (stackTrace != null) { | ||||
|       debugPrint('StackTrace: $stackTrace'); | ||||
|     } | ||||
|  | ||||
|     final isar = Isar.getInstance(); | ||||
|     if (isar == null) { | ||||
|       debugPrint('Isar is not initialized'); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     final record = LogMessage( | ||||
|       message: message, | ||||
|       level: LogLevel.info, | ||||
|       createdAt: DateTime.now(), | ||||
|       logger: kDevLoggerTag, | ||||
|       error: error?.toString(), | ||||
|       stack: stackTrace?.toString(), | ||||
|     ); | ||||
|  | ||||
|     unawaited(IsarLogRepository(isar).insert(record)); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										174
									
								
								mobile/lib/presentation/pages/dev/feat_in_development.page.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								mobile/lib/presentation/pages/dev/feat_in_development.page.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,174 @@ | ||||
| // ignore_for_file: avoid-local-functions | ||||
|  | ||||
| import 'dart:async'; | ||||
|  | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:drift/drift.dart' hide Column; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| import 'package:immich_mobile/extensions/theme_extensions.dart'; | ||||
| import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart'; | ||||
| import 'package:immich_mobile/providers/background_sync.provider.dart'; | ||||
| import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; | ||||
| import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; | ||||
| import 'package:immich_mobile/routing/router.dart'; | ||||
|  | ||||
| final _features = [ | ||||
|   _Feature( | ||||
|     name: 'Sync Local', | ||||
|     icon: Icons.photo_album_rounded, | ||||
|     onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(), | ||||
|   ), | ||||
|   _Feature( | ||||
|     name: 'Sync Local Full', | ||||
|     icon: Icons.photo_library_rounded, | ||||
|     onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(full: true), | ||||
|   ), | ||||
|   _Feature( | ||||
|     name: 'Sync Remote', | ||||
|     icon: Icons.refresh_rounded, | ||||
|     onTap: (_, ref) => ref.read(backgroundSyncProvider).syncRemote(), | ||||
|   ), | ||||
|   _Feature( | ||||
|     name: 'WAL Checkpoint', | ||||
|     icon: Icons.save_rounded, | ||||
|     onTap: (_, ref) => ref | ||||
|         .read(driftProvider) | ||||
|         .customStatement("pragma wal_checkpoint(truncate)"), | ||||
|   ), | ||||
|   _Feature( | ||||
|     name: 'Clear Delta Checkpoint', | ||||
|     icon: Icons.delete_rounded, | ||||
|     onTap: (_, ref) => ref.read(nativeSyncApiProvider).clearSyncCheckpoint(), | ||||
|   ), | ||||
|   _Feature( | ||||
|     name: 'Clear Local Data', | ||||
|     icon: Icons.delete_forever_rounded, | ||||
|     onTap: (_, ref) async { | ||||
|       final db = ref.read(driftProvider); | ||||
|       await db.localAssetEntity.deleteAll(); | ||||
|       await db.localAlbumEntity.deleteAll(); | ||||
|       await db.localAlbumAssetEntity.deleteAll(); | ||||
|     }, | ||||
|   ), | ||||
|   _Feature( | ||||
|     name: 'Local Media Summary', | ||||
|     icon: Icons.table_chart_rounded, | ||||
|     onTap: (ctx, _) => ctx.pushRoute(const LocalMediaSummaryRoute()), | ||||
|   ), | ||||
| ]; | ||||
|  | ||||
| @RoutePage() | ||||
| class FeatInDevPage extends StatelessWidget { | ||||
|   const FeatInDevPage({super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|       appBar: AppBar( | ||||
|         title: const Text('Features in Development'), | ||||
|         centerTitle: true, | ||||
|       ), | ||||
|       body: Column( | ||||
|         children: [ | ||||
|           Flexible( | ||||
|             flex: 1, | ||||
|             child: ListView.builder( | ||||
|               itemBuilder: (_, index) { | ||||
|                 final feat = _features[index]; | ||||
|                 return Consumer( | ||||
|                   builder: (ctx, ref, _) => ListTile( | ||||
|                     title: Text(feat.name), | ||||
|                     trailing: Icon(feat.icon), | ||||
|                     visualDensity: VisualDensity.compact, | ||||
|                     onTap: () => unawaited(feat.onTap(ctx, ref)), | ||||
|                   ), | ||||
|                 ); | ||||
|               }, | ||||
|               itemCount: _features.length, | ||||
|             ), | ||||
|           ), | ||||
|           const Divider(height: 0), | ||||
|           const Flexible(child: _DevLogs()), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _Feature { | ||||
|   const _Feature({ | ||||
|     required this.name, | ||||
|     required this.icon, | ||||
|     required this.onTap, | ||||
|   }); | ||||
|  | ||||
|   final String name; | ||||
|   final IconData icon; | ||||
|   final Future<void> Function(BuildContext, WidgetRef _) onTap; | ||||
| } | ||||
|  | ||||
| // ignore: prefer-single-widget-per-file | ||||
| class _DevLogs extends StatelessWidget { | ||||
|   const _DevLogs(); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|       appBar: AppBar( | ||||
|         automaticallyImplyLeading: false, | ||||
|         actions: [ | ||||
|           IconButton( | ||||
|             onPressed: DLog.clearLog, | ||||
|             icon: Icon( | ||||
|               Icons.delete_outline_rounded, | ||||
|               size: 20.0, | ||||
|               color: context.primaryColor, | ||||
|               semanticLabel: "Clear logs", | ||||
|             ), | ||||
|           ), | ||||
|         ], | ||||
|         centerTitle: true, | ||||
|       ), | ||||
|       body: StreamBuilder( | ||||
|         initialData: [], | ||||
|         stream: DLog.watchLog(), | ||||
|         builder: (_, logMessages) { | ||||
|           return ListView.separated( | ||||
|             itemBuilder: (ctx, index) { | ||||
|               // ignore: avoid-unsafe-collection-methods | ||||
|               final logMessage = logMessages.data![index]; | ||||
|               return ListTile( | ||||
|                 title: Text( | ||||
|                   logMessage.message, | ||||
|                   style: TextStyle( | ||||
|                     color: ctx.colorScheme.onSurface, | ||||
|                     fontSize: 14.0, | ||||
|                     overflow: TextOverflow.ellipsis, | ||||
|                   ), | ||||
|                 ), | ||||
|                 subtitle: Text( | ||||
|                   "at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)}", | ||||
|                   style: TextStyle( | ||||
|                     color: ctx.colorScheme.onSurfaceSecondary, | ||||
|                     fontSize: 12.0, | ||||
|                   ), | ||||
|                 ), | ||||
|                 dense: true, | ||||
|                 visualDensity: VisualDensity.compact, | ||||
|                 tileColor: Colors.transparent, | ||||
|                 minLeadingWidth: 10, | ||||
|               ); | ||||
|             }, | ||||
|             separatorBuilder: (_, index) { | ||||
|               return const Divider(height: 0); | ||||
|             }, | ||||
|             itemCount: logMessages.data?.length ?? 0, | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										125
									
								
								mobile/lib/presentation/pages/dev/local_media_stat.page.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								mobile/lib/presentation/pages/dev/local_media_stat.page.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,125 @@ | ||||
| import 'package:auto_route/auto_route.dart'; | ||||
| import 'package:collection/collection.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/domain/models/local_album.model.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; | ||||
| import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; | ||||
| import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; | ||||
|  | ||||
| final _stats = [ | ||||
|   _Stat( | ||||
|     name: 'Local Assets', | ||||
|     load: (db) => db.managers.localAssetEntity.count(), | ||||
|   ), | ||||
|   _Stat( | ||||
|     name: 'Local Albums', | ||||
|     load: (db) => db.managers.localAlbumEntity.count(), | ||||
|   ), | ||||
| ]; | ||||
|  | ||||
| @RoutePage() | ||||
| class LocalMediaSummaryPage extends StatelessWidget { | ||||
|   const LocalMediaSummaryPage({super.key}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
|       appBar: AppBar(title: const Text('Local Media Summary')), | ||||
|       body: Consumer( | ||||
|         builder: (ctx, ref, __) { | ||||
|           final db = ref.watch(driftProvider); | ||||
|           final albumsFuture = ref.watch(localAlbumRepository).getAll(); | ||||
|  | ||||
|           return CustomScrollView( | ||||
|             slivers: [ | ||||
|               SliverList.builder( | ||||
|                 itemBuilder: (_, index) { | ||||
|                   final stat = _stats[index]; | ||||
|                   final countFuture = stat.load(db); | ||||
|                   return _Summary(name: stat.name, countFuture: countFuture); | ||||
|                 }, | ||||
|                 itemCount: _stats.length, | ||||
|               ), | ||||
|               SliverToBoxAdapter( | ||||
|                 child: Column( | ||||
|                   crossAxisAlignment: CrossAxisAlignment.stretch, | ||||
|                   children: [ | ||||
|                     const Divider(), | ||||
|                     Padding( | ||||
|                       padding: const EdgeInsets.only(left: 15), | ||||
|                       child: Text( | ||||
|                         "Album summary", | ||||
|                         style: ctx.textTheme.titleMedium, | ||||
|                       ), | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|               FutureBuilder( | ||||
|                 future: albumsFuture, | ||||
|                 initialData: <LocalAlbum>[], | ||||
|                 builder: (_, snap) { | ||||
|                   final albums = snap.data!; | ||||
|                   if (albums.isEmpty) { | ||||
|                     return const SliverToBoxAdapter(child: SizedBox.shrink()); | ||||
|                   } | ||||
|  | ||||
|                   albums.sortBy((a) => a.name); | ||||
|                   return SliverList.builder( | ||||
|                     itemBuilder: (_, index) { | ||||
|                       final album = albums[index]; | ||||
|                       final countFuture = db.managers.localAlbumAssetEntity | ||||
|                           .filter((f) => f.albumId.id.equals(album.id)) | ||||
|                           .count(); | ||||
|                       return _Summary( | ||||
|                         name: album.name, | ||||
|                         countFuture: countFuture, | ||||
|                       ); | ||||
|                     }, | ||||
|                     itemCount: albums.length, | ||||
|                   ); | ||||
|                 }, | ||||
|               ), | ||||
|             ], | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // ignore: prefer-single-widget-per-file | ||||
| class _Summary extends StatelessWidget { | ||||
|   final String name; | ||||
|   final Future<int> countFuture; | ||||
|  | ||||
|   const _Summary({required this.name, required this.countFuture}); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return FutureBuilder<int>( | ||||
|       future: countFuture, | ||||
|       builder: (ctx, snapshot) { | ||||
|         final Widget subtitle; | ||||
|  | ||||
|         if (snapshot.connectionState == ConnectionState.waiting) { | ||||
|           subtitle = const CircularProgressIndicator(); | ||||
|         } else if (snapshot.hasError) { | ||||
|           subtitle = const Icon(Icons.error_rounded); | ||||
|         } else { | ||||
|           subtitle = Text('${snapshot.data ?? 0}'); | ||||
|         } | ||||
|         return ListTile(title: Text(name), trailing: subtitle); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _Stat { | ||||
|   const _Stat({required this.name, required this.load}); | ||||
|  | ||||
|   final String name; | ||||
|   final Future<int> Function(Drift _) load; | ||||
| } | ||||
							
								
								
									
										8
									
								
								mobile/lib/providers/infrastructure/album.provider.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								mobile/lib/providers/infrastructure/album.provider.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; | ||||
| import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; | ||||
| import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; | ||||
|  | ||||
| final localAlbumRepository = Provider<ILocalAlbumRepository>( | ||||
|   (ref) => DriftLocalAlbumRepository(ref.watch(driftProvider)), | ||||
| ); | ||||
| @@ -0,0 +1,4 @@ | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/platform/native_sync_api.g.dart'; | ||||
|  | ||||
| final nativeSyncApiProvider = Provider<NativeSyncApi>((_) => NativeSyncApi()); | ||||
| @@ -1,10 +1,14 @@ | ||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/domain/services/local_sync.service.dart'; | ||||
| import 'package:immich_mobile/domain/services/sync_stream.service.dart'; | ||||
| import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; | ||||
| import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; | ||||
| import 'package:immich_mobile/providers/api.provider.dart'; | ||||
| import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; | ||||
| import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; | ||||
| import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; | ||||
| import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; | ||||
| import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; | ||||
| 
 | ||||
| final syncStreamServiceProvider = Provider( | ||||
|   (ref) => SyncStreamService( | ||||
| @@ -21,3 +25,11 @@ final syncApiRepositoryProvider = Provider( | ||||
| final syncStreamRepositoryProvider = Provider( | ||||
|   (ref) => DriftSyncStreamRepository(ref.watch(driftProvider)), | ||||
| ); | ||||
| 
 | ||||
| final localSyncServiceProvider = Provider( | ||||
|   (ref) => LocalSyncService( | ||||
|     localAlbumRepository: ref.watch(localAlbumRepository), | ||||
|     nativeSyncApi: ref.watch(nativeSyncApiProvider), | ||||
|     storeService: ref.watch(storeServiceProvider), | ||||
|   ), | ||||
| ); | ||||
| @@ -63,6 +63,8 @@ import 'package:immich_mobile/pages/search/person_result.page.dart'; | ||||
| import 'package:immich_mobile/pages/search/recently_taken.page.dart'; | ||||
| import 'package:immich_mobile/pages/search/search.page.dart'; | ||||
| import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; | ||||
| import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart'; | ||||
| import 'package:immich_mobile/presentation/pages/dev/local_media_stat.page.dart'; | ||||
| import 'package:immich_mobile/providers/api.provider.dart'; | ||||
| import 'package:immich_mobile/providers/gallery_permission.provider.dart'; | ||||
| import 'package:immich_mobile/routing/auth_guard.dart'; | ||||
| @@ -316,5 +318,13 @@ class AppRouter extends RootStackRouter { | ||||
|       page: PinAuthRoute.page, | ||||
|       guards: [_authGuard, _duplicateGuard], | ||||
|     ), | ||||
|     AutoRoute( | ||||
|       page: FeatInDevRoute.page, | ||||
|       guards: [_authGuard, _duplicateGuard], | ||||
|     ), | ||||
|     AutoRoute( | ||||
|       page: LocalMediaSummaryRoute.page, | ||||
|       guards: [_authGuard, _duplicateGuard], | ||||
|     ), | ||||
|   ]; | ||||
| } | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| // dart format width=80 | ||||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||||
|  | ||||
| // ************************************************************************** | ||||
| @@ -13,10 +14,7 @@ part of 'router.dart'; | ||||
| /// [ActivitiesPage] | ||||
| class ActivitiesRoute extends PageRouteInfo<void> { | ||||
|   const ActivitiesRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           ActivitiesRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(ActivitiesRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'ActivitiesRoute'; | ||||
|  | ||||
| @@ -132,10 +130,7 @@ class AlbumAssetSelectionRouteArgs { | ||||
| /// [AlbumOptionsPage] | ||||
| class AlbumOptionsRoute extends PageRouteInfo<void> { | ||||
|   const AlbumOptionsRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           AlbumOptionsRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(AlbumOptionsRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'AlbumOptionsRoute'; | ||||
|  | ||||
| @@ -156,10 +151,7 @@ class AlbumPreviewRoute extends PageRouteInfo<AlbumPreviewRouteArgs> { | ||||
|     List<PageRouteInfo>? children, | ||||
|   }) : super( | ||||
|           AlbumPreviewRoute.name, | ||||
|           args: AlbumPreviewRouteArgs( | ||||
|             key: key, | ||||
|             album: album, | ||||
|           ), | ||||
|           args: AlbumPreviewRouteArgs(key: key, album: album), | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|  | ||||
| @@ -169,19 +161,13 @@ class AlbumPreviewRoute extends PageRouteInfo<AlbumPreviewRouteArgs> { | ||||
|     name, | ||||
|     builder: (data) { | ||||
|       final args = data.argsAs<AlbumPreviewRouteArgs>(); | ||||
|       return AlbumPreviewPage( | ||||
|         key: args.key, | ||||
|         album: args.album, | ||||
|       ); | ||||
|       return AlbumPreviewPage(key: args.key, album: args.album); | ||||
|     }, | ||||
|   ); | ||||
| } | ||||
|  | ||||
| class AlbumPreviewRouteArgs { | ||||
|   const AlbumPreviewRouteArgs({ | ||||
|     this.key, | ||||
|     required this.album, | ||||
|   }); | ||||
|   const AlbumPreviewRouteArgs({this.key, required this.album}); | ||||
|  | ||||
|   final Key? key; | ||||
|  | ||||
| @@ -203,10 +189,7 @@ class AlbumSharedUserSelectionRoute | ||||
|     List<PageRouteInfo>? children, | ||||
|   }) : super( | ||||
|           AlbumSharedUserSelectionRoute.name, | ||||
|           args: AlbumSharedUserSelectionRouteArgs( | ||||
|             key: key, | ||||
|             assets: assets, | ||||
|           ), | ||||
|           args: AlbumSharedUserSelectionRouteArgs(key: key, assets: assets), | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|  | ||||
| @@ -216,19 +199,13 @@ class AlbumSharedUserSelectionRoute | ||||
|     name, | ||||
|     builder: (data) { | ||||
|       final args = data.argsAs<AlbumSharedUserSelectionRouteArgs>(); | ||||
|       return AlbumSharedUserSelectionPage( | ||||
|         key: args.key, | ||||
|         assets: args.assets, | ||||
|       ); | ||||
|       return AlbumSharedUserSelectionPage(key: args.key, assets: args.assets); | ||||
|     }, | ||||
|   ); | ||||
| } | ||||
|  | ||||
| class AlbumSharedUserSelectionRouteArgs { | ||||
|   const AlbumSharedUserSelectionRouteArgs({ | ||||
|     this.key, | ||||
|     required this.assets, | ||||
|   }); | ||||
|   const AlbumSharedUserSelectionRouteArgs({this.key, required this.assets}); | ||||
|  | ||||
|   final Key? key; | ||||
|  | ||||
| @@ -249,10 +226,7 @@ class AlbumViewerRoute extends PageRouteInfo<AlbumViewerRouteArgs> { | ||||
|     List<PageRouteInfo>? children, | ||||
|   }) : super( | ||||
|           AlbumViewerRoute.name, | ||||
|           args: AlbumViewerRouteArgs( | ||||
|             key: key, | ||||
|             albumId: albumId, | ||||
|           ), | ||||
|           args: AlbumViewerRouteArgs(key: key, albumId: albumId), | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|  | ||||
| @@ -262,19 +236,13 @@ class AlbumViewerRoute extends PageRouteInfo<AlbumViewerRouteArgs> { | ||||
|     name, | ||||
|     builder: (data) { | ||||
|       final args = data.argsAs<AlbumViewerRouteArgs>(); | ||||
|       return AlbumViewerPage( | ||||
|         key: args.key, | ||||
|         albumId: args.albumId, | ||||
|       ); | ||||
|       return AlbumViewerPage(key: args.key, albumId: args.albumId); | ||||
|     }, | ||||
|   ); | ||||
| } | ||||
|  | ||||
| class AlbumViewerRouteArgs { | ||||
|   const AlbumViewerRouteArgs({ | ||||
|     this.key, | ||||
|     required this.albumId, | ||||
|   }); | ||||
|   const AlbumViewerRouteArgs({this.key, required this.albumId}); | ||||
|  | ||||
|   final Key? key; | ||||
|  | ||||
| @@ -290,10 +258,7 @@ class AlbumViewerRouteArgs { | ||||
| /// [AlbumsPage] | ||||
| class AlbumsRoute extends PageRouteInfo<void> { | ||||
|   const AlbumsRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           AlbumsRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(AlbumsRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'AlbumsRoute'; | ||||
|  | ||||
| @@ -309,10 +274,7 @@ class AlbumsRoute extends PageRouteInfo<void> { | ||||
| /// [AllMotionPhotosPage] | ||||
| class AllMotionPhotosRoute extends PageRouteInfo<void> { | ||||
|   const AllMotionPhotosRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           AllMotionPhotosRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(AllMotionPhotosRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'AllMotionPhotosRoute'; | ||||
|  | ||||
| @@ -328,10 +290,7 @@ class AllMotionPhotosRoute extends PageRouteInfo<void> { | ||||
| /// [AllPeoplePage] | ||||
| class AllPeopleRoute extends PageRouteInfo<void> { | ||||
|   const AllPeopleRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           AllPeopleRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(AllPeopleRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'AllPeopleRoute'; | ||||
|  | ||||
| @@ -347,10 +306,7 @@ class AllPeopleRoute extends PageRouteInfo<void> { | ||||
| /// [AllPlacesPage] | ||||
| class AllPlacesRoute extends PageRouteInfo<void> { | ||||
|   const AllPlacesRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           AllPlacesRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(AllPlacesRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'AllPlacesRoute'; | ||||
|  | ||||
| @@ -366,10 +322,7 @@ class AllPlacesRoute extends PageRouteInfo<void> { | ||||
| /// [AllVideosPage] | ||||
| class AllVideosRoute extends PageRouteInfo<void> { | ||||
|   const AllVideosRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           AllVideosRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(AllVideosRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'AllVideosRoute'; | ||||
|  | ||||
| @@ -390,10 +343,7 @@ class AppLogDetailRoute extends PageRouteInfo<AppLogDetailRouteArgs> { | ||||
|     List<PageRouteInfo>? children, | ||||
|   }) : super( | ||||
|           AppLogDetailRoute.name, | ||||
|           args: AppLogDetailRouteArgs( | ||||
|             key: key, | ||||
|             logMessage: logMessage, | ||||
|           ), | ||||
|           args: AppLogDetailRouteArgs(key: key, logMessage: logMessage), | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|  | ||||
| @@ -403,19 +353,13 @@ class AppLogDetailRoute extends PageRouteInfo<AppLogDetailRouteArgs> { | ||||
|     name, | ||||
|     builder: (data) { | ||||
|       final args = data.argsAs<AppLogDetailRouteArgs>(); | ||||
|       return AppLogDetailPage( | ||||
|         key: args.key, | ||||
|         logMessage: args.logMessage, | ||||
|       ); | ||||
|       return AppLogDetailPage(key: args.key, logMessage: args.logMessage); | ||||
|     }, | ||||
|   ); | ||||
| } | ||||
|  | ||||
| class AppLogDetailRouteArgs { | ||||
|   const AppLogDetailRouteArgs({ | ||||
|     this.key, | ||||
|     required this.logMessage, | ||||
|   }); | ||||
|   const AppLogDetailRouteArgs({this.key, required this.logMessage}); | ||||
|  | ||||
|   final Key? key; | ||||
|  | ||||
| @@ -431,10 +375,7 @@ class AppLogDetailRouteArgs { | ||||
| /// [AppLogPage] | ||||
| class AppLogRoute extends PageRouteInfo<void> { | ||||
|   const AppLogRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           AppLogRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(AppLogRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'AppLogRoute'; | ||||
|  | ||||
| @@ -450,10 +391,7 @@ class AppLogRoute extends PageRouteInfo<void> { | ||||
| /// [ArchivePage] | ||||
| class ArchiveRoute extends PageRouteInfo<void> { | ||||
|   const ArchiveRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           ArchiveRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(ArchiveRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'ArchiveRoute'; | ||||
|  | ||||
| @@ -469,10 +407,7 @@ class ArchiveRoute extends PageRouteInfo<void> { | ||||
| /// [BackupAlbumSelectionPage] | ||||
| class BackupAlbumSelectionRoute extends PageRouteInfo<void> { | ||||
|   const BackupAlbumSelectionRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           BackupAlbumSelectionRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(BackupAlbumSelectionRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'BackupAlbumSelectionRoute'; | ||||
|  | ||||
| @@ -488,10 +423,7 @@ class BackupAlbumSelectionRoute extends PageRouteInfo<void> { | ||||
| /// [BackupControllerPage] | ||||
| class BackupControllerRoute extends PageRouteInfo<void> { | ||||
|   const BackupControllerRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           BackupControllerRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(BackupControllerRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'BackupControllerRoute'; | ||||
|  | ||||
| @@ -507,10 +439,7 @@ class BackupControllerRoute extends PageRouteInfo<void> { | ||||
| /// [BackupOptionsPage] | ||||
| class BackupOptionsRoute extends PageRouteInfo<void> { | ||||
|   const BackupOptionsRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           BackupOptionsRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(BackupOptionsRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'BackupOptionsRoute'; | ||||
|  | ||||
| @@ -526,10 +455,7 @@ class BackupOptionsRoute extends PageRouteInfo<void> { | ||||
| /// [ChangePasswordPage] | ||||
| class ChangePasswordRoute extends PageRouteInfo<void> { | ||||
|   const ChangePasswordRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           ChangePasswordRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(ChangePasswordRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'ChangePasswordRoute'; | ||||
|  | ||||
| @@ -550,10 +476,7 @@ class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> { | ||||
|     List<PageRouteInfo>? children, | ||||
|   }) : super( | ||||
|           CreateAlbumRoute.name, | ||||
|           args: CreateAlbumRouteArgs( | ||||
|             key: key, | ||||
|             assets: assets, | ||||
|           ), | ||||
|           args: CreateAlbumRouteArgs(key: key, assets: assets), | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|  | ||||
| @@ -563,20 +486,15 @@ class CreateAlbumRoute extends PageRouteInfo<CreateAlbumRouteArgs> { | ||||
|     name, | ||||
|     builder: (data) { | ||||
|       final args = data.argsAs<CreateAlbumRouteArgs>( | ||||
|           orElse: () => const CreateAlbumRouteArgs()); | ||||
|       return CreateAlbumPage( | ||||
|         key: args.key, | ||||
|         assets: args.assets, | ||||
|         orElse: () => const CreateAlbumRouteArgs(), | ||||
|       ); | ||||
|       return CreateAlbumPage(key: args.key, assets: args.assets); | ||||
|     }, | ||||
|   ); | ||||
| } | ||||
|  | ||||
| class CreateAlbumRouteArgs { | ||||
|   const CreateAlbumRouteArgs({ | ||||
|     this.key, | ||||
|     this.assets, | ||||
|   }); | ||||
|   const CreateAlbumRouteArgs({this.key, this.assets}); | ||||
|  | ||||
|   final Key? key; | ||||
|  | ||||
| @@ -598,11 +516,7 @@ class CropImageRoute extends PageRouteInfo<CropImageRouteArgs> { | ||||
|     List<PageRouteInfo>? children, | ||||
|   }) : super( | ||||
|           CropImageRoute.name, | ||||
|           args: CropImageRouteArgs( | ||||
|             key: key, | ||||
|             image: image, | ||||
|             asset: asset, | ||||
|           ), | ||||
|           args: CropImageRouteArgs(key: key, image: image, asset: asset), | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|  | ||||
| @@ -612,11 +526,7 @@ class CropImageRoute extends PageRouteInfo<CropImageRouteArgs> { | ||||
|     name, | ||||
|     builder: (data) { | ||||
|       final args = data.argsAs<CropImageRouteArgs>(); | ||||
|       return CropImagePage( | ||||
|         key: args.key, | ||||
|         image: args.image, | ||||
|         asset: args.asset, | ||||
|       ); | ||||
|       return CropImagePage(key: args.key, image: args.image, asset: args.asset); | ||||
|     }, | ||||
|   ); | ||||
| } | ||||
| @@ -702,10 +612,7 @@ class EditImageRouteArgs { | ||||
| /// [FailedBackupStatusPage] | ||||
| class FailedBackupStatusRoute extends PageRouteInfo<void> { | ||||
|   const FailedBackupStatusRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           FailedBackupStatusRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(FailedBackupStatusRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'FailedBackupStatusRoute'; | ||||
|  | ||||
| @@ -721,10 +628,7 @@ class FailedBackupStatusRoute extends PageRouteInfo<void> { | ||||
| /// [FavoritesPage] | ||||
| class FavoritesRoute extends PageRouteInfo<void> { | ||||
|   const FavoritesRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           FavoritesRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(FavoritesRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'FavoritesRoute'; | ||||
|  | ||||
| @@ -736,6 +640,22 @@ class FavoritesRoute extends PageRouteInfo<void> { | ||||
|   ); | ||||
| } | ||||
|  | ||||
| /// generated route for | ||||
| /// [FeatInDevPage] | ||||
| class FeatInDevRoute extends PageRouteInfo<void> { | ||||
|   const FeatInDevRoute({List<PageRouteInfo>? children}) | ||||
|       : super(FeatInDevRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'FeatInDevRoute'; | ||||
|  | ||||
|   static PageInfo page = PageInfo( | ||||
|     name, | ||||
|     builder: (data) { | ||||
|       return const FeatInDevPage(); | ||||
|     }, | ||||
|   ); | ||||
| } | ||||
|  | ||||
| /// generated route for | ||||
| /// [FilterImagePage] | ||||
| class FilterImageRoute extends PageRouteInfo<FilterImageRouteArgs> { | ||||
| @@ -746,11 +666,7 @@ class FilterImageRoute extends PageRouteInfo<FilterImageRouteArgs> { | ||||
|     List<PageRouteInfo>? children, | ||||
|   }) : super( | ||||
|           FilterImageRoute.name, | ||||
|           args: FilterImageRouteArgs( | ||||
|             key: key, | ||||
|             image: image, | ||||
|             asset: asset, | ||||
|           ), | ||||
|           args: FilterImageRouteArgs(key: key, image: image, asset: asset), | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|  | ||||
| @@ -797,10 +713,7 @@ class FolderRoute extends PageRouteInfo<FolderRouteArgs> { | ||||
|     List<PageRouteInfo>? children, | ||||
|   }) : super( | ||||
|           FolderRoute.name, | ||||
|           args: FolderRouteArgs( | ||||
|             key: key, | ||||
|             folder: folder, | ||||
|           ), | ||||
|           args: FolderRouteArgs(key: key, folder: folder), | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|  | ||||
| @@ -809,21 +722,16 @@ class FolderRoute extends PageRouteInfo<FolderRouteArgs> { | ||||
|   static PageInfo page = PageInfo( | ||||
|     name, | ||||
|     builder: (data) { | ||||
|       final args = | ||||
|           data.argsAs<FolderRouteArgs>(orElse: () => const FolderRouteArgs()); | ||||
|       return FolderPage( | ||||
|         key: args.key, | ||||
|         folder: args.folder, | ||||
|       final args = data.argsAs<FolderRouteArgs>( | ||||
|         orElse: () => const FolderRouteArgs(), | ||||
|       ); | ||||
|       return FolderPage(key: args.key, folder: args.folder); | ||||
|     }, | ||||
|   ); | ||||
| } | ||||
|  | ||||
| class FolderRouteArgs { | ||||
|   const FolderRouteArgs({ | ||||
|     this.key, | ||||
|     this.folder, | ||||
|   }); | ||||
|   const FolderRouteArgs({this.key, this.folder}); | ||||
|  | ||||
|   final Key? key; | ||||
|  | ||||
| @@ -903,10 +811,7 @@ class GalleryViewerRouteArgs { | ||||
| /// [HeaderSettingsPage] | ||||
| class HeaderSettingsRoute extends PageRouteInfo<void> { | ||||
|   const HeaderSettingsRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           HeaderSettingsRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(HeaderSettingsRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'HeaderSettingsRoute'; | ||||
|  | ||||
| @@ -922,10 +827,7 @@ class HeaderSettingsRoute extends PageRouteInfo<void> { | ||||
| /// [LibraryPage] | ||||
| class LibraryRoute extends PageRouteInfo<void> { | ||||
|   const LibraryRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           LibraryRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(LibraryRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'LibraryRoute'; | ||||
|  | ||||
| @@ -941,10 +843,7 @@ class LibraryRoute extends PageRouteInfo<void> { | ||||
| /// [LocalAlbumsPage] | ||||
| class LocalAlbumsRoute extends PageRouteInfo<void> { | ||||
|   const LocalAlbumsRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           LocalAlbumsRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(LocalAlbumsRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'LocalAlbumsRoute'; | ||||
|  | ||||
| @@ -956,14 +855,27 @@ class LocalAlbumsRoute extends PageRouteInfo<void> { | ||||
|   ); | ||||
| } | ||||
|  | ||||
| /// generated route for | ||||
| /// [LocalMediaSummaryPage] | ||||
| class LocalMediaSummaryRoute extends PageRouteInfo<void> { | ||||
|   const LocalMediaSummaryRoute({List<PageRouteInfo>? children}) | ||||
|       : super(LocalMediaSummaryRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'LocalMediaSummaryRoute'; | ||||
|  | ||||
|   static PageInfo page = PageInfo( | ||||
|     name, | ||||
|     builder: (data) { | ||||
|       return const LocalMediaSummaryPage(); | ||||
|     }, | ||||
|   ); | ||||
| } | ||||
|  | ||||
| /// generated route for | ||||
| /// [LockedPage] | ||||
| class LockedRoute extends PageRouteInfo<void> { | ||||
|   const LockedRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           LockedRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(LockedRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'LockedRoute'; | ||||
|  | ||||
| @@ -979,10 +891,7 @@ class LockedRoute extends PageRouteInfo<void> { | ||||
| /// [LoginPage] | ||||
| class LoginRoute extends PageRouteInfo<void> { | ||||
|   const LoginRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           LoginRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(LoginRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'LoginRoute'; | ||||
|  | ||||
| @@ -1016,7 +925,8 @@ class MapLocationPickerRoute extends PageRouteInfo<MapLocationPickerRouteArgs> { | ||||
|     name, | ||||
|     builder: (data) { | ||||
|       final args = data.argsAs<MapLocationPickerRouteArgs>( | ||||
|           orElse: () => const MapLocationPickerRouteArgs()); | ||||
|         orElse: () => const MapLocationPickerRouteArgs(), | ||||
|       ); | ||||
|       return MapLocationPickerPage( | ||||
|         key: args.key, | ||||
|         initialLatLng: args.initialLatLng, | ||||
| @@ -1044,16 +954,10 @@ class MapLocationPickerRouteArgs { | ||||
| /// generated route for | ||||
| /// [MapPage] | ||||
| class MapRoute extends PageRouteInfo<MapRouteArgs> { | ||||
|   MapRoute({ | ||||
|     Key? key, | ||||
|     LatLng? initialLocation, | ||||
|     List<PageRouteInfo>? children, | ||||
|   }) : super( | ||||
|   MapRoute({Key? key, LatLng? initialLocation, List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           MapRoute.name, | ||||
|           args: MapRouteArgs( | ||||
|             key: key, | ||||
|             initialLocation: initialLocation, | ||||
|           ), | ||||
|           args: MapRouteArgs(key: key, initialLocation: initialLocation), | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|  | ||||
| @@ -1062,21 +966,16 @@ class MapRoute extends PageRouteInfo<MapRouteArgs> { | ||||
|   static PageInfo page = PageInfo( | ||||
|     name, | ||||
|     builder: (data) { | ||||
|       final args = | ||||
|           data.argsAs<MapRouteArgs>(orElse: () => const MapRouteArgs()); | ||||
|       return MapPage( | ||||
|         key: args.key, | ||||
|         initialLocation: args.initialLocation, | ||||
|       final args = data.argsAs<MapRouteArgs>( | ||||
|         orElse: () => const MapRouteArgs(), | ||||
|       ); | ||||
|       return MapPage(key: args.key, initialLocation: args.initialLocation); | ||||
|     }, | ||||
|   ); | ||||
| } | ||||
|  | ||||
| class MapRouteArgs { | ||||
|   const MapRouteArgs({ | ||||
|     this.key, | ||||
|     this.initialLocation, | ||||
|   }); | ||||
|   const MapRouteArgs({this.key, this.initialLocation}); | ||||
|  | ||||
|   final Key? key; | ||||
|  | ||||
| @@ -1213,10 +1112,7 @@ class PartnerDetailRoute extends PageRouteInfo<PartnerDetailRouteArgs> { | ||||
|     List<PageRouteInfo>? children, | ||||
|   }) : super( | ||||
|           PartnerDetailRoute.name, | ||||
|           args: PartnerDetailRouteArgs( | ||||
|             key: key, | ||||
|             partner: partner, | ||||
|           ), | ||||
|           args: PartnerDetailRouteArgs(key: key, partner: partner), | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|  | ||||
| @@ -1226,19 +1122,13 @@ class PartnerDetailRoute extends PageRouteInfo<PartnerDetailRouteArgs> { | ||||
|     name, | ||||
|     builder: (data) { | ||||
|       final args = data.argsAs<PartnerDetailRouteArgs>(); | ||||
|       return PartnerDetailPage( | ||||
|         key: args.key, | ||||
|         partner: args.partner, | ||||
|       ); | ||||
|       return PartnerDetailPage(key: args.key, partner: args.partner); | ||||
|     }, | ||||
|   ); | ||||
| } | ||||
|  | ||||
| class PartnerDetailRouteArgs { | ||||
|   const PartnerDetailRouteArgs({ | ||||
|     this.key, | ||||
|     required this.partner, | ||||
|   }); | ||||
|   const PartnerDetailRouteArgs({this.key, required this.partner}); | ||||
|  | ||||
|   final Key? key; | ||||
|  | ||||
| @@ -1254,10 +1144,7 @@ class PartnerDetailRouteArgs { | ||||
| /// [PartnerPage] | ||||
| class PartnerRoute extends PageRouteInfo<void> { | ||||
|   const PartnerRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           PartnerRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(PartnerRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'PartnerRoute'; | ||||
|  | ||||
| @@ -1273,10 +1160,7 @@ class PartnerRoute extends PageRouteInfo<void> { | ||||
| /// [PeopleCollectionPage] | ||||
| class PeopleCollectionRoute extends PageRouteInfo<void> { | ||||
|   const PeopleCollectionRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           PeopleCollectionRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(PeopleCollectionRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'PeopleCollectionRoute'; | ||||
|  | ||||
| @@ -1292,10 +1176,7 @@ class PeopleCollectionRoute extends PageRouteInfo<void> { | ||||
| /// [PermissionOnboardingPage] | ||||
| class PermissionOnboardingRoute extends PageRouteInfo<void> { | ||||
|   const PermissionOnboardingRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           PermissionOnboardingRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(PermissionOnboardingRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'PermissionOnboardingRoute'; | ||||
|  | ||||
| @@ -1363,10 +1244,7 @@ class PersonResultRouteArgs { | ||||
| /// [PhotosPage] | ||||
| class PhotosRoute extends PageRouteInfo<void> { | ||||
|   const PhotosRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           PhotosRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(PhotosRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'PhotosRoute'; | ||||
|  | ||||
| @@ -1387,10 +1265,7 @@ class PinAuthRoute extends PageRouteInfo<PinAuthRouteArgs> { | ||||
|     List<PageRouteInfo>? children, | ||||
|   }) : super( | ||||
|           PinAuthRoute.name, | ||||
|           args: PinAuthRouteArgs( | ||||
|             key: key, | ||||
|             createPinCode: createPinCode, | ||||
|           ), | ||||
|           args: PinAuthRouteArgs(key: key, createPinCode: createPinCode), | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|  | ||||
| @@ -1399,21 +1274,16 @@ class PinAuthRoute extends PageRouteInfo<PinAuthRouteArgs> { | ||||
|   static PageInfo page = PageInfo( | ||||
|     name, | ||||
|     builder: (data) { | ||||
|       final args = | ||||
|           data.argsAs<PinAuthRouteArgs>(orElse: () => const PinAuthRouteArgs()); | ||||
|       return PinAuthPage( | ||||
|         key: args.key, | ||||
|         createPinCode: args.createPinCode, | ||||
|       final args = data.argsAs<PinAuthRouteArgs>( | ||||
|         orElse: () => const PinAuthRouteArgs(), | ||||
|       ); | ||||
|       return PinAuthPage(key: args.key, createPinCode: args.createPinCode); | ||||
|     }, | ||||
|   ); | ||||
| } | ||||
|  | ||||
| class PinAuthRouteArgs { | ||||
|   const PinAuthRouteArgs({ | ||||
|     this.key, | ||||
|     this.createPinCode = false, | ||||
|   }); | ||||
|   const PinAuthRouteArgs({this.key, this.createPinCode = false}); | ||||
|  | ||||
|   final Key? key; | ||||
|  | ||||
| @@ -1447,7 +1317,8 @@ class PlacesCollectionRoute extends PageRouteInfo<PlacesCollectionRouteArgs> { | ||||
|     name, | ||||
|     builder: (data) { | ||||
|       final args = data.argsAs<PlacesCollectionRouteArgs>( | ||||
|           orElse: () => const PlacesCollectionRouteArgs()); | ||||
|         orElse: () => const PlacesCollectionRouteArgs(), | ||||
|       ); | ||||
|       return PlacesCollectionPage( | ||||
|         key: args.key, | ||||
|         currentLocation: args.currentLocation, | ||||
| @@ -1457,10 +1328,7 @@ class PlacesCollectionRoute extends PageRouteInfo<PlacesCollectionRouteArgs> { | ||||
| } | ||||
|  | ||||
| class PlacesCollectionRouteArgs { | ||||
|   const PlacesCollectionRouteArgs({ | ||||
|     this.key, | ||||
|     this.currentLocation, | ||||
|   }); | ||||
|   const PlacesCollectionRouteArgs({this.key, this.currentLocation}); | ||||
|  | ||||
|   final Key? key; | ||||
|  | ||||
| @@ -1476,10 +1344,7 @@ class PlacesCollectionRouteArgs { | ||||
| /// [RecentlyTakenPage] | ||||
| class RecentlyTakenRoute extends PageRouteInfo<void> { | ||||
|   const RecentlyTakenRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           RecentlyTakenRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(RecentlyTakenRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'RecentlyTakenRoute'; | ||||
|  | ||||
| @@ -1500,10 +1365,7 @@ class SearchRoute extends PageRouteInfo<SearchRouteArgs> { | ||||
|     List<PageRouteInfo>? children, | ||||
|   }) : super( | ||||
|           SearchRoute.name, | ||||
|           args: SearchRouteArgs( | ||||
|             key: key, | ||||
|             prefilter: prefilter, | ||||
|           ), | ||||
|           args: SearchRouteArgs(key: key, prefilter: prefilter), | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|  | ||||
| @@ -1512,21 +1374,16 @@ class SearchRoute extends PageRouteInfo<SearchRouteArgs> { | ||||
|   static PageInfo page = PageInfo( | ||||
|     name, | ||||
|     builder: (data) { | ||||
|       final args = | ||||
|           data.argsAs<SearchRouteArgs>(orElse: () => const SearchRouteArgs()); | ||||
|       return SearchPage( | ||||
|         key: args.key, | ||||
|         prefilter: args.prefilter, | ||||
|       final args = data.argsAs<SearchRouteArgs>( | ||||
|         orElse: () => const SearchRouteArgs(), | ||||
|       ); | ||||
|       return SearchPage(key: args.key, prefilter: args.prefilter); | ||||
|     }, | ||||
|   ); | ||||
| } | ||||
|  | ||||
| class SearchRouteArgs { | ||||
|   const SearchRouteArgs({ | ||||
|     this.key, | ||||
|     this.prefilter, | ||||
|   }); | ||||
|   const SearchRouteArgs({this.key, this.prefilter}); | ||||
|  | ||||
|   final Key? key; | ||||
|  | ||||
| @@ -1542,10 +1399,7 @@ class SearchRouteArgs { | ||||
| /// [SettingsPage] | ||||
| class SettingsRoute extends PageRouteInfo<void> { | ||||
|   const SettingsRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           SettingsRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(SettingsRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'SettingsRoute'; | ||||
|  | ||||
| @@ -1566,10 +1420,7 @@ class SettingsSubRoute extends PageRouteInfo<SettingsSubRouteArgs> { | ||||
|     List<PageRouteInfo>? children, | ||||
|   }) : super( | ||||
|           SettingsSubRoute.name, | ||||
|           args: SettingsSubRouteArgs( | ||||
|             section: section, | ||||
|             key: key, | ||||
|           ), | ||||
|           args: SettingsSubRouteArgs(section: section, key: key), | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|  | ||||
| @@ -1579,19 +1430,13 @@ class SettingsSubRoute extends PageRouteInfo<SettingsSubRouteArgs> { | ||||
|     name, | ||||
|     builder: (data) { | ||||
|       final args = data.argsAs<SettingsSubRouteArgs>(); | ||||
|       return SettingsSubPage( | ||||
|         args.section, | ||||
|         key: args.key, | ||||
|       ); | ||||
|       return SettingsSubPage(args.section, key: args.key); | ||||
|     }, | ||||
|   ); | ||||
| } | ||||
|  | ||||
| class SettingsSubRouteArgs { | ||||
|   const SettingsSubRouteArgs({ | ||||
|     required this.section, | ||||
|     this.key, | ||||
|   }); | ||||
|   const SettingsSubRouteArgs({required this.section, this.key}); | ||||
|  | ||||
|   final SettingSection section; | ||||
|  | ||||
| @@ -1612,10 +1457,7 @@ class ShareIntentRoute extends PageRouteInfo<ShareIntentRouteArgs> { | ||||
|     List<PageRouteInfo>? children, | ||||
|   }) : super( | ||||
|           ShareIntentRoute.name, | ||||
|           args: ShareIntentRouteArgs( | ||||
|             key: key, | ||||
|             attachments: attachments, | ||||
|           ), | ||||
|           args: ShareIntentRouteArgs(key: key, attachments: attachments), | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|  | ||||
| @@ -1625,19 +1467,13 @@ class ShareIntentRoute extends PageRouteInfo<ShareIntentRouteArgs> { | ||||
|     name, | ||||
|     builder: (data) { | ||||
|       final args = data.argsAs<ShareIntentRouteArgs>(); | ||||
|       return ShareIntentPage( | ||||
|         key: args.key, | ||||
|         attachments: args.attachments, | ||||
|       ); | ||||
|       return ShareIntentPage(key: args.key, attachments: args.attachments); | ||||
|     }, | ||||
|   ); | ||||
| } | ||||
|  | ||||
| class ShareIntentRouteArgs { | ||||
|   const ShareIntentRouteArgs({ | ||||
|     this.key, | ||||
|     required this.attachments, | ||||
|   }); | ||||
|   const ShareIntentRouteArgs({this.key, required this.attachments}); | ||||
|  | ||||
|   final Key? key; | ||||
|  | ||||
| @@ -1675,7 +1511,8 @@ class SharedLinkEditRoute extends PageRouteInfo<SharedLinkEditRouteArgs> { | ||||
|     name, | ||||
|     builder: (data) { | ||||
|       final args = data.argsAs<SharedLinkEditRouteArgs>( | ||||
|           orElse: () => const SharedLinkEditRouteArgs()); | ||||
|         orElse: () => const SharedLinkEditRouteArgs(), | ||||
|       ); | ||||
|       return SharedLinkEditPage( | ||||
|         key: args.key, | ||||
|         existingLink: args.existingLink, | ||||
| @@ -1712,10 +1549,7 @@ class SharedLinkEditRouteArgs { | ||||
| /// [SharedLinkPage] | ||||
| class SharedLinkRoute extends PageRouteInfo<void> { | ||||
|   const SharedLinkRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           SharedLinkRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(SharedLinkRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'SharedLinkRoute'; | ||||
|  | ||||
| @@ -1731,10 +1565,7 @@ class SharedLinkRoute extends PageRouteInfo<void> { | ||||
| /// [SplashScreenPage] | ||||
| class SplashScreenRoute extends PageRouteInfo<void> { | ||||
|   const SplashScreenRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           SplashScreenRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(SplashScreenRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'SplashScreenRoute'; | ||||
|  | ||||
| @@ -1750,10 +1581,7 @@ class SplashScreenRoute extends PageRouteInfo<void> { | ||||
| /// [TabControllerPage] | ||||
| class TabControllerRoute extends PageRouteInfo<void> { | ||||
|   const TabControllerRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           TabControllerRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(TabControllerRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'TabControllerRoute'; | ||||
|  | ||||
| @@ -1769,10 +1597,7 @@ class TabControllerRoute extends PageRouteInfo<void> { | ||||
| /// [TrashPage] | ||||
| class TrashRoute extends PageRouteInfo<void> { | ||||
|   const TrashRoute({List<PageRouteInfo>? children}) | ||||
|       : super( | ||||
|           TrashRoute.name, | ||||
|           initialChildren: children, | ||||
|         ); | ||||
|       : super(TrashRoute.name, initialChildren: children); | ||||
|  | ||||
|   static const String name = 'TrashRoute'; | ||||
|  | ||||
|   | ||||
| @@ -7,7 +7,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||
| import 'package:immich_mobile/extensions/build_context_extensions.dart'; | ||||
| import 'package:immich_mobile/models/backup/backup_state.model.dart'; | ||||
| import 'package:immich_mobile/models/server_info/server_info.model.dart'; | ||||
| import 'package:immich_mobile/providers/background_sync.provider.dart'; | ||||
| import 'package:immich_mobile/providers/backup/backup.provider.dart'; | ||||
| import 'package:immich_mobile/providers/server_info.provider.dart'; | ||||
| import 'package:immich_mobile/providers/user.provider.dart'; | ||||
| @@ -180,10 +179,10 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { | ||||
|               child: action, | ||||
|             ), | ||||
|           ), | ||||
|         if (kDebugMode) | ||||
|         if (kDebugMode || kProfileMode) | ||||
|           IconButton( | ||||
|             onPressed: () => ref.read(backgroundSyncProvider).sync(), | ||||
|             icon: const Icon(Icons.sync), | ||||
|             icon: const Icon(Icons.science_rounded), | ||||
|             onPressed: () => context.pushRoute(const FeatInDevRoute()), | ||||
|           ), | ||||
|         if (showUploadButton) | ||||
|           Padding( | ||||
|   | ||||
| @@ -1,7 +1,13 @@ | ||||
| .PHONY: build watch create_app_icon create_splash build_release_android | ||||
| .PHONY: build watch create_app_icon create_splash build_release_android pigeon | ||||
|  | ||||
| build: | ||||
| 	dart run build_runner build --delete-conflicting-outputs | ||||
| # Remove once auto_route updated to 10.1.0 | ||||
| 	dart format lib/routing/router.gr.dart | ||||
|  | ||||
| pigeon: | ||||
| 	dart run pigeon --input pigeon/native_sync_api.dart | ||||
| 	dart format lib/platform/native_sync_api.g.dart | ||||
|  | ||||
| watch: | ||||
| 	dart run build_runner watch --delete-conflicting-outputs | ||||
| @@ -19,4 +25,5 @@ migrations: | ||||
| 	dart run drift_dev make-migrations | ||||
|  | ||||
| translation: | ||||
| 	dart run easy_localization:generate -S ../i18n  | ||||
| 	dart run easy_localization:generate -S ../i18n | ||||
| 	dart format lib/generated/codegen_loader.g.dart | ||||
							
								
								
									
										89
									
								
								mobile/pigeon/native_sync_api.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								mobile/pigeon/native_sync_api.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | ||||
| import 'package:pigeon/pigeon.dart'; | ||||
|  | ||||
| @ConfigurePigeon( | ||||
|   PigeonOptions( | ||||
|     dartOut: 'lib/platform/native_sync_api.g.dart', | ||||
|     swiftOut: 'ios/Runner/Sync/Messages.g.swift', | ||||
|     swiftOptions: SwiftOptions(), | ||||
|     kotlinOut: | ||||
|         'android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt', | ||||
|     kotlinOptions: KotlinOptions(package: 'app.alextran.immich.sync'), | ||||
|     dartOptions: DartOptions(), | ||||
|     dartPackageName: 'immich_mobile', | ||||
|   ), | ||||
| ) | ||||
| class PlatformAsset { | ||||
|   final String id; | ||||
|   final String name; | ||||
|   // Follows AssetType enum from base_asset.model.dart | ||||
|   final int type; | ||||
|   // Seconds since epoch | ||||
|   final int? createdAt; | ||||
|   final int? updatedAt; | ||||
|   final int durationInSeconds; | ||||
|  | ||||
|   const PlatformAsset({ | ||||
|     required this.id, | ||||
|     required this.name, | ||||
|     required this.type, | ||||
|     this.createdAt, | ||||
|     this.updatedAt, | ||||
|     this.durationInSeconds = 0, | ||||
|   }); | ||||
| } | ||||
|  | ||||
| class PlatformAlbum { | ||||
|   final String id; | ||||
|   final String name; | ||||
|   // Seconds since epoch | ||||
|   final int? updatedAt; | ||||
|   final bool isCloud; | ||||
|   final int assetCount; | ||||
|  | ||||
|   const PlatformAlbum({ | ||||
|     required this.id, | ||||
|     required this.name, | ||||
|     this.updatedAt, | ||||
|     this.isCloud = false, | ||||
|     this.assetCount = 0, | ||||
|   }); | ||||
| } | ||||
|  | ||||
| class SyncDelta { | ||||
|   final bool hasChanges; | ||||
|   final List<PlatformAsset> updates; | ||||
|   final List<String> deletes; | ||||
|   // Asset -> Album mapping | ||||
|   final Map<String, List<String>> assetAlbums; | ||||
|  | ||||
|   const SyncDelta({ | ||||
|     this.hasChanges = false, | ||||
|     this.updates = const [], | ||||
|     this.deletes = const [], | ||||
|     this.assetAlbums = const {}, | ||||
|   }); | ||||
| } | ||||
|  | ||||
| @HostApi() | ||||
| abstract class NativeSyncApi { | ||||
|   bool shouldFullSync(); | ||||
|  | ||||
|   @TaskQueue(type: TaskQueueType.serialBackgroundThread) | ||||
|   SyncDelta getMediaChanges(); | ||||
|  | ||||
|   void checkpointSync(); | ||||
|  | ||||
|   void clearSyncCheckpoint(); | ||||
|  | ||||
|   @TaskQueue(type: TaskQueueType.serialBackgroundThread) | ||||
|   List<String> getAssetIdsForAlbum(String albumId); | ||||
|  | ||||
|   @TaskQueue(type: TaskQueueType.serialBackgroundThread) | ||||
|   List<PlatformAlbum> getAlbums(); | ||||
|  | ||||
|   @TaskQueue(type: TaskQueueType.serialBackgroundThread) | ||||
|   int getAssetsCountSince(String albumId, int timestamp); | ||||
|  | ||||
|   @TaskQueue(type: TaskQueueType.serialBackgroundThread) | ||||
|   List<PlatformAsset> getAssetsForAlbum(String albumId, {int? updatedTimeCond}); | ||||
| } | ||||
| @@ -5,31 +5,26 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: _fe_analyzer_shared | ||||
|       sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" | ||||
|       sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "76.0.0" | ||||
|   _macros: | ||||
|     dependency: transitive | ||||
|     description: dart | ||||
|     source: sdk | ||||
|     version: "0.3.3" | ||||
|     version: "80.0.0" | ||||
|   analyzer: | ||||
|     dependency: "direct overridden" | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: analyzer | ||||
|       sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" | ||||
|       sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "6.11.0" | ||||
|     version: "7.3.0" | ||||
|   analyzer_plugin: | ||||
|     dependency: "direct overridden" | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: analyzer_plugin | ||||
|       sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" | ||||
|       sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.11.3" | ||||
|     version: "0.13.0" | ||||
|   ansicolor: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -74,10 +69,10 @@ packages: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
|       name: auto_route_generator | ||||
|       sha256: c9086eb07271e51b44071ad5cff34e889f3156710b964a308c2ab590769e79e6 | ||||
|       sha256: c2e359d8932986d4d1bcad7a428143f81384ce10fef8d4aa5bc29e1f83766a46 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "9.0.0" | ||||
|     version: "9.3.1" | ||||
|   background_downloader: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -322,34 +317,42 @@ packages: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
|       name: custom_lint | ||||
|       sha256: "4500e88854e7581ee43586abeaf4443cb22375d6d289241a87b1aadf678d5545" | ||||
|       sha256: "409c485fd14f544af1da965d5a0d160ee57cd58b63eeaa7280a4f28cf5bda7f1" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.6.10" | ||||
|     version: "0.7.5" | ||||
|   custom_lint_builder: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: custom_lint_builder | ||||
|       sha256: "5a95eff100da256fbf086b329c17c8b49058c261cdf56d3a4157d3c31c511d78" | ||||
|       sha256: "107e0a43606138015777590ee8ce32f26ba7415c25b722ff0908a6f5d7a4c228" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.6.10" | ||||
|     version: "0.7.5" | ||||
|   custom_lint_core: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: custom_lint_core | ||||
|       sha256: "76a4046cc71d976222a078a8fd4a65e198b70545a8d690a75196dd14f08510f6" | ||||
|       sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.6.10" | ||||
|     version: "0.7.5" | ||||
|   custom_lint_visitor: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: custom_lint_visitor | ||||
|       sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.0.0+7.3.0" | ||||
|   dart_style: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: dart_style | ||||
|       sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" | ||||
|       sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.3.8" | ||||
|     version: "3.1.0" | ||||
|   dartx: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -723,10 +726,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: freezed_annotation | ||||
|       sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 | ||||
|       sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.4.4" | ||||
|     version: "3.0.0" | ||||
|   frontend_server_client: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -971,10 +974,11 @@ packages: | ||||
|   isar_generator: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
|       name: isar_generator | ||||
|       sha256: "484e73d3b7e81dbd816852fe0b9497333118a9aeb646fd2d349a62cc8980ffe1" | ||||
|       url: "https://pub.isar-community.dev" | ||||
|     source: hosted | ||||
|       path: "packages/isar_generator" | ||||
|       ref: v3 | ||||
|       resolved-ref: ad574f60ed6f39d2995cd16fc7dc3de9a646ef30 | ||||
|       url: "https://github.com/immich-app/isar" | ||||
|     source: git | ||||
|     version: "3.1.8" | ||||
|   js: | ||||
|     dependency: transitive | ||||
| @@ -1072,14 +1076,6 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.3.0" | ||||
|   macros: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: macros | ||||
|       sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.1.3-main.0" | ||||
|   maplibre_gl: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -1121,7 +1117,7 @@ packages: | ||||
|     source: hosted | ||||
|     version: "0.11.1" | ||||
|   meta: | ||||
|     dependency: "direct overridden" | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: meta | ||||
|       sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c | ||||
| @@ -1352,6 +1348,14 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.2.0" | ||||
|   pigeon: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
|       name: pigeon | ||||
|       sha256: a093af76026160bb5ff6eb98e3e678a301ffd1001ac0d90be558bc133a0c73f5 | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "25.3.2" | ||||
|   pinput: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -1361,7 +1365,7 @@ packages: | ||||
|     source: hosted | ||||
|     version: "5.0.1" | ||||
|   platform: | ||||
|     dependency: transitive | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: platform | ||||
|       sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" | ||||
| @@ -1444,10 +1448,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: riverpod_analyzer_utils | ||||
|       sha256: "0dcb0af32d561f8fa000c6a6d95633c9fb08ea8a8df46e3f9daca59f11218167" | ||||
|       sha256: "03a17170088c63aab6c54c44456f5ab78876a1ddb6032ffde1662ddab4959611" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.5.6" | ||||
|     version: "0.5.10" | ||||
|   riverpod_annotation: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
| @@ -1460,18 +1464,18 @@ packages: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
|       name: riverpod_generator | ||||
|       sha256: "851aedac7ad52693d12af3bf6d92b1626d516ed6b764eb61bf19e968b5e0b931" | ||||
|       sha256: "44a0992d54473eb199ede00e2260bd3c262a86560e3c6f6374503d86d0580e36" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.6.1" | ||||
|     version: "2.6.5" | ||||
|   riverpod_lint: | ||||
|     dependency: "direct dev" | ||||
|     description: | ||||
|       name: riverpod_lint | ||||
|       sha256: "0684c21a9a4582c28c897d55c7b611fa59a351579061b43f8c92c005804e63a8" | ||||
|       sha256: "89a52b7334210dbff8605c3edf26cfe69b15062beed5cbfeff2c3812c33c9e35" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "2.6.1" | ||||
|     version: "2.6.5" | ||||
|   rxdart: | ||||
|     dependency: transitive | ||||
|     description: | ||||
| @@ -1633,10 +1637,10 @@ packages: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|       name: source_gen | ||||
|       sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" | ||||
|       sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "1.5.0" | ||||
|     version: "2.0.0" | ||||
|   source_span: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|   | ||||
| @@ -32,6 +32,7 @@ dependencies: | ||||
|   flutter_displaymode: ^0.6.0 | ||||
|   flutter_hooks: ^0.21.2 | ||||
|   flutter_local_notifications: ^17.2.1+2 | ||||
|   flutter_secure_storage: ^9.2.4 | ||||
|   flutter_svg: ^2.0.17 | ||||
|   flutter_udid: ^3.0.0 | ||||
|   flutter_web_auth_2: ^5.0.0-alpha.0 | ||||
| @@ -41,6 +42,7 @@ dependencies: | ||||
|   http: ^1.3.0 | ||||
|   image_picker: ^1.1.2 | ||||
|   intl: ^0.19.0 | ||||
|   local_auth: ^2.3.0 | ||||
|   logging: ^1.3.0 | ||||
|   maplibre_gl: ^0.21.0 | ||||
|   network_info_plus: ^6.1.3 | ||||
| @@ -52,6 +54,8 @@ dependencies: | ||||
|   permission_handler: ^11.4.0 | ||||
|   photo_manager: ^3.6.4 | ||||
|   photo_manager_image_provider: ^2.2.0 | ||||
|   pinput: ^5.0.1 | ||||
|   platform: ^3.1.6 | ||||
|   punycode: ^1.0.0 | ||||
|   riverpod_annotation: ^2.6.1 | ||||
|   scrollable_positioned_list: ^0.3.8 | ||||
| @@ -64,9 +68,6 @@ dependencies: | ||||
|   uuid: ^4.5.1 | ||||
|   wakelock_plus: ^1.2.10 | ||||
|   worker_manager: ^7.2.3 | ||||
|   local_auth: ^2.3.0 | ||||
|   pinput: ^5.0.1 | ||||
|   flutter_secure_storage: ^9.2.4 | ||||
|  | ||||
|   native_video_player: | ||||
|     git: | ||||
| @@ -84,11 +85,6 @@ dependencies: | ||||
|   drift: ^2.23.1 | ||||
|   drift_flutter: ^0.2.4 | ||||
|  | ||||
| dependency_overrides: | ||||
|   analyzer: ^6.0.0 | ||||
|   meta: ^1.11.0 | ||||
|   analyzer_plugin: ^0.11.3 | ||||
|  | ||||
| dev_dependencies: | ||||
|   flutter_test: | ||||
|     sdk: flutter | ||||
| @@ -98,11 +94,13 @@ dev_dependencies: | ||||
|   flutter_launcher_icons: ^0.14.3 | ||||
|   flutter_native_splash: ^2.4.5 | ||||
|   isar_generator: | ||||
|     version: *isar_version | ||||
|     hosted: https://pub.isar-community.dev/ | ||||
|     git: | ||||
|       url: https://github.com/immich-app/isar | ||||
|       ref: v3 | ||||
|       path: packages/isar_generator/ | ||||
|   integration_test: | ||||
|     sdk: flutter | ||||
|   custom_lint: ^0.6.4 | ||||
|   custom_lint: ^0.7.5 | ||||
|   riverpod_lint: ^2.6.1 | ||||
|   riverpod_generator: ^2.6.1 | ||||
|   mocktail: ^1.0.4 | ||||
| @@ -112,6 +110,8 @@ dev_dependencies: | ||||
|   file: ^7.0.1 # for MemoryFileSystem | ||||
|   # Drift generator | ||||
|   drift_dev: ^2.23.1 | ||||
|   # Type safe platform code | ||||
|   pigeon: ^25.3.1 | ||||
|  | ||||
| flutter: | ||||
|   uses-material-design: true | ||||
|   | ||||
		Reference in New Issue
	
	Block a user