diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 0ec511d9f1..f537b1b9d1 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -3,6 +3,7 @@ plugins { id "kotlin-android" id "dev.flutter.flutter-gradle-plugin" id 'com.google.devtools.ksp' + id 'org.jetbrains.kotlin.plugin.serialization' } def localProperties = new Properties() @@ -89,6 +90,7 @@ dependencies { 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" @@ -96,6 +98,7 @@ dependencies { 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' } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt index 2b6bf81148..a197922f1a 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt @@ -2,12 +2,12 @@ package app.alextran.immich import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine -import androidx.annotation.NonNull +import app.alextran.immich.platform.MessagesImpl class MainActivity : FlutterActivity() { - override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) flutterEngine.plugins.add(BackgroundServicePlugin()) - // No need to set up method channel here as it's now handled in the plugin + ImHostService.setUp(flutterEngine.dartExecutor.binaryMessenger, MessagesImpl(this)) } } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/platform/MediaManager.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/platform/MediaManager.kt new file mode 100644 index 0000000000..565382fed5 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/platform/MediaManager.kt @@ -0,0 +1,238 @@ +package app.alextran.immich.platform + +import Asset +import SyncDelta +import android.content.Context +import android.os.Build +import android.provider.MediaStore +import android.provider.MediaStore.VOLUME_EXTERNAL +import androidx.annotation.RequiresApi +import androidx.annotation.RequiresExtension +import androidx.core.content.edit +import kotlinx.serialization.json.Json +import java.io.File +import java.time.Instant +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter + +class MediaManager(context: Context) { + private val ctx: Context = context.applicationContext + + companion object { + private const val SHARED_PREF_NAME = "Immich::MediaManager" + private const val SHARED_PREF_MEDIA_STORE_VERSION_KEY = "MediaStore::getVersion" + private const val SHARED_PREF_MEDIA_STORE_GEN_KEY = "MediaStore::getGeneration" + + private fun getSavedGenerationMap(context: Context): Map { + return context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) + .getString(SHARED_PREF_MEDIA_STORE_GEN_KEY, null)?.let { + Json.decodeFromString>(it) + } ?: emptyMap() + } + } + + @RequiresApi(Build.VERSION_CODES.Q) + fun shouldFullSync(callback: (Result) -> Unit) { + val currVersion = MediaStore.getVersion(ctx) + val lastVersion = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) + .getString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, null) + callback(Result.success(currVersion != lastVersion)) + } + + @RequiresApi(Build.VERSION_CODES.Q) + @RequiresExtension(extension = Build.VERSION_CODES.R, version = 1) + fun hasMediaChanges(callback: (Result) -> Unit) { + val genMap = getSavedGenerationMap(ctx) + val currentVolumes = MediaStore.getExternalVolumeNames(ctx) + if (currentVolumes != genMap.keys) { + callback(Result.success(true)) + return + } + + val hasChanged = currentVolumes.any { volume -> + val currentGen = MediaStore.getGeneration(ctx, volume) + currentGen != genMap[volume] + } + callback(Result.success(hasChanged)) + } + + @RequiresApi(Build.VERSION_CODES.Q) + @RequiresExtension(extension = Build.VERSION_CODES.R, version = 1) + fun checkpointSync(callback: (Result) -> Unit) { + val genMap = MediaStore.getExternalVolumeNames(ctx).associateWith { + MediaStore.getGeneration(ctx, it) + }.let { Json.encodeToString(it) } + + ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE).edit { + putString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, MediaStore.getVersion(ctx)) + putString(SHARED_PREF_MEDIA_STORE_GEN_KEY, genMap) + } + callback(Result.success(Unit)) + } + + @RequiresApi(Build.VERSION_CODES.Q) + fun getAssetIdsForAlbum( + albumId: String, + callback: (Result>) -> Unit + ) { + try { + val currentIds = mutableListOf() + val uri = MediaStore.Files.getContentUri(VOLUME_EXTERNAL) + val projection = arrayOf(MediaStore.Files.FileColumns._ID) + val selection = """ + ${MediaStore.Files.FileColumns.BUCKET_ID} = ? + AND ( + ${MediaStore.Files.FileColumns.MEDIA_TYPE} = ? + OR ${MediaStore.Files.FileColumns.MEDIA_TYPE} = ? + ) + """.trimIndent() + val selectionArgs = arrayOf( + albumId, + MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString(), + MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString() + ) + val sortOrder = null + + ctx.contentResolver.query( + uri, + projection, + selection, + selectionArgs, + sortOrder + )?.use { cursor -> + val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID) + while (cursor.moveToNext()) { + currentIds.add(cursor.getLong(idColumn).toString()) + } + } + + callback(Result.success(currentIds)) + } catch (e: Exception) { + callback(Result.failure(e)) + } + } + + @RequiresApi(Build.VERSION_CODES.R) + @RequiresExtension(extension = Build.VERSION_CODES.R, version = 1) + fun getMediaChanges(callback: (Result) -> Unit) { + try { + val genMap = getSavedGenerationMap(ctx) + val currentVolumes = MediaStore.getExternalVolumeNames(ctx) + val changed = mutableListOf() + val deleted = mutableListOf() + + for (volume in currentVolumes) { + val currentGen = MediaStore.getGeneration(ctx, volume) + val storedGen = genMap[volume] + + if (storedGen != null && currentGen < storedGen) { + continue + } + + val queryUri = MediaStore.Files.getContentUri(volume) + val projection = arrayOf( + MediaStore.MediaColumns._ID, + MediaStore.MediaColumns.DATA, + MediaStore.MediaColumns.DISPLAY_NAME, + MediaStore.MediaColumns.TITLE, + MediaStore.Files.FileColumns.MEDIA_TYPE, + MediaStore.MediaColumns.BUCKET_ID, + MediaStore.MediaColumns.DURATION, + MediaStore.MediaColumns.DATE_TAKEN, + MediaStore.MediaColumns.DATE_ADDED, + MediaStore.MediaColumns.DATE_MODIFIED, + ) + + val selectionParts = mutableListOf() + val selectionArgsList = mutableListOf() + + selectionParts.add( + "(${MediaStore.Files.FileColumns.MEDIA_TYPE} = ? OR ${MediaStore.Files.FileColumns.MEDIA_TYPE} = ?)" + ) + selectionArgsList.add(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString()) + selectionArgsList.add(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString()) + + if (storedGen != null) { + selectionParts.add( + "(${MediaStore.MediaColumns.GENERATION_MODIFIED} > ? OR ${MediaStore.MediaColumns.GENERATION_ADDED} > ?)" + ) + selectionArgsList.add(storedGen.toString()) + selectionArgsList.add(storedGen.toString()) + } + + val selection = selectionParts.joinToString(" AND ") + val selectionArgs = selectionArgsList.toTypedArray() + val sortOrder = null + + ctx.contentResolver.query( + queryUri, + projection, + selection, + selectionArgs, + sortOrder + )?.use { cursor -> + val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID) + val mediaTypeColumn = + cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE) + val nameColumn = + cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME) + val dataColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA) + val dateTakeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_TAKEN) + val dateAddedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED) + val dateModifiedColumn = + cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED) + val durationColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION) + val bucketIdColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.BUCKET_ID) + val formatter = DateTimeFormatter.ISO_DATE_TIME.withZone(ZoneOffset.UTC) + + while (cursor.moveToNext()) { + val id = cursor.getLong(idColumn).toString() + val path = cursor.getString(dataColumn) + if (path.isBlank() || !File(path).exists()) { + deleted.add(id) + continue + } + + val mediaType = cursor.getInt(mediaTypeColumn) + val name = cursor.getString(nameColumn) + // Date taken is milliseconds since epoch + var takenAt = cursor.getLong(dateTakeColumn) + if (takenAt == 0L) { + // Date added is seconds since epoch + takenAt = cursor.getLong(dateAddedColumn) * 1000 + } + val takenInstant = Instant.ofEpochMilli(takenAt) + val createdAt = formatter.format(takenInstant) + // Date modified is seconds since epoch + val modifiedAt = + formatter.format(Instant.ofEpochMilli(cursor.getLong(dateModifiedColumn) * 1000)) + // Duration is milliseconds + val duration = + if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0 else cursor.getLong( + durationColumn + ) / 1000 + val bucketId = cursor.getString(bucketIdColumn) + + changed.add( + Asset( + id, + name, + mediaType.toLong(), + createdAt, + modifiedAt, + duration, + listOf(bucketId) + ) + ) + } + } + } + // Unmounted volumes are handled in dart when the album is removed + val syncDelta = SyncDelta(updates = changed, deletes = deleted) + callback(Result.success(syncDelta)) + + } catch (e: Exception) { + callback(Result.failure(e)) + } + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/platform/messages.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/platform/Messages.g.kt similarity index 89% rename from mobile/android/app/src/main/kotlin/app/alextran/immich/platform/messages.g.kt rename to mobile/android/app/src/main/kotlin/app/alextran/immich/platform/Messages.g.kt index 49b165de70..b6b3a8ab3a 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/platform/messages.g.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/platform/Messages.g.kt @@ -153,7 +153,7 @@ data class SyncDelta ( override fun hashCode(): Int = toList().hashCode() } -private open class messagesPigeonCodec : StandardMessageCodec() { +private open class MessagesPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { return when (type) { 129.toByte() -> { @@ -191,11 +191,12 @@ interface ImHostService { fun hasMediaChanges(callback: (Result) -> Unit) fun getMediaChanges(callback: (Result) -> Unit) fun checkpointSync(callback: (Result) -> Unit) + fun getAssetIdsForAlbum(albumId: String, callback: (Result>) -> Unit) companion object { /** The codec used by ImHostService. */ val codec: MessageCodec by lazy { - messagesPigeonCodec() + MessagesPigeonCodec() } /** Sets up an instance of `ImHostService` to handle messages through the `binaryMessenger`. */ @JvmOverloads @@ -273,6 +274,26 @@ interface ImHostService { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostService.getAssetIdsForAlbum$separatedMessageChannelSuffix", codec, taskQueue) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val albumIdArg = args[0] as String + api.getAssetIdsForAlbum(albumIdArg) { result: Result> -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(MessagesPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(MessagesPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } } } } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/platform/MessagesImpl.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/platform/MessagesImpl.kt new file mode 100644 index 0000000000..a4963365d0 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/platform/MessagesImpl.kt @@ -0,0 +1,55 @@ +package app.alextran.immich.platform + +import ImHostService +import SyncDelta +import android.content.Context +import android.os.Build +import android.os.ext.SdkExtensions + +class MessagesImpl(context: Context) : ImHostService { + private val ctx: Context = context.applicationContext + private val mediaManager: MediaManager = MediaManager(ctx) + + override fun shouldFullSync(callback: (Result) -> Unit) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + mediaManager.shouldFullSync(callback) + } else { + callback(Result.success(true)) + } + } + + override fun hasMediaChanges(callback: (Result) -> Unit) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R) >= 1) { + mediaManager.hasMediaChanges(callback) + } else { + callback(Result.failure(IllegalStateException("hasMediaChanges changes not supported on this Android version."))) + } + } + + override fun getMediaChanges(callback: (Result) -> Unit) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R) >= 1) { + mediaManager.getMediaChanges(callback) + } else { + callback(Result.failure(IllegalStateException("getMediaChanges not supported on this Android version."))) + } + } + + override fun checkpointSync(callback: (Result) -> Unit) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R) >= 1) { + mediaManager.checkpointSync(callback) + } else { + callback(Result.success(Unit)) + } + } + + override fun getAssetIdsForAlbum( + albumId: String, + callback: (Result>) -> Unit + ) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R) >= 1) { + mediaManager.getAssetIdsForAlbum(albumId, callback) + } else { + callback(Result.failure(IllegalStateException("getAssetIdsForAlbum not supported on this Android version."))) + } + } +} diff --git a/mobile/android/settings.gradle b/mobile/android/settings.gradle index 74f8904a10..969d1d008a 100644 --- a/mobile/android/settings.gradle +++ b/mobile/android/settings.gradle @@ -20,6 +20,7 @@ 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 '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 } diff --git a/mobile/drift_schemas/main/drift_schema_v1.json b/mobile/drift_schemas/main/drift_schema_v1.json index b9fc57de4d..c45348a184 100644 --- a/mobile/drift_schemas/main/drift_schema_v1.json +++ b/mobile/drift_schemas/main/drift_schema_v1.json @@ -1 +1 @@ -{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"preferences","getter_name":"preferences","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userPreferenceConverter","dart_type_name":"UserPreferences"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}},{"id":3,"references":[],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":4,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"local_id","getter_name":"localId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["local_id"]}},{"id":5,"references":[4,3],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (local_id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (local_id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":6,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"remote_id","getter_name":"remoteId","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"UNIQUE","dialectAwareDefaultConstraints":{"sqlite":"UNIQUE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumbhash","getter_name":"thumbhash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["remote_id"]}},{"id":7,"references":[6],"type":"table","data":{"name":"exif_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (remote_id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (remote_id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"city","getter_name":"city","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"state","getter_name":"state","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"country","getter_name":"country","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"date_time_original","getter_name":"dateTimeOriginal","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"exposure_time","getter_name":"exposureTime","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"f_number","getter_name":"fNumber","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"file_size","getter_name":"fileSize","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"focal_length","getter_name":"focalLength","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"iso","getter_name":"iso","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"make","getter_name":"make","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"model","getter_name":"model","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"time_zone","getter_name":"timeZone","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"rating","getter_name":"rating","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"projection_type","getter_name":"projectionType","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":8,"references":[4],"type":"index","data":{"on":4,"name":"local_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":9,"references":[6],"type":"index","data":{"on":6,"name":"remote_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}}]} \ No newline at end of file +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"preferences","getter_name":"preferences","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userPreferenceConverter","dart_type_name":"UserPreferences"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}},{"id":3,"references":[],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":4,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":5,"references":[4,3],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":6,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"blob","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"UNIQUE","dialectAwareDefaultConstraints":{"sqlite":"UNIQUE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumbhash","getter_name":"thumbhash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":7,"references":[6],"type":"table","data":{"name":"exif_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"blob","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"city","getter_name":"city","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"state","getter_name":"state","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"country","getter_name":"country","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"date_time_original","getter_name":"dateTimeOriginal","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"exposure_time","getter_name":"exposureTime","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"f_number","getter_name":"fNumber","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"file_size","getter_name":"fileSize","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"focal_length","getter_name":"focalLength","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"iso","getter_name":"iso","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"make","getter_name":"make","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"model","getter_name":"model","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"time_zone","getter_name":"timeZone","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"rating","getter_name":"rating","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"projection_type","getter_name":"projectionType","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":8,"references":[4],"type":"index","data":{"on":4,"name":"local_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":9,"references":[6],"type":"index","data":{"on":6,"name":"remote_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}}]} \ No newline at end of file diff --git a/mobile/ios/Runner/Platform/Messages.g.swift b/mobile/ios/Runner/Platform/Messages.g.swift index 0534f9a4ce..de9e4974ab 100644 --- a/mobile/ios/Runner/Platform/Messages.g.swift +++ b/mobile/ios/Runner/Platform/Messages.g.swift @@ -254,6 +254,7 @@ protocol ImHostService { func hasMediaChanges(completion: @escaping (Result) -> Void) func getMediaChanges(completion: @escaping (Result) -> Void) func checkpointSync(completion: @escaping (Result) -> Void) + func getAssetIdsForAlbum(albumId: String, completion: @escaping (Result<[String], Error>) -> Void) } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. @@ -329,5 +330,24 @@ class ImHostServiceSetup { } else { checkpointSyncChannel.setMessageHandler(nil) } + let getAssetIdsForAlbumChannel = taskQueue == nil + ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostService.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostService.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 + api.getAssetIdsForAlbum(albumId: albumIdArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + getAssetIdsForAlbumChannel.setMessageHandler(nil) + } } } diff --git a/mobile/ios/Runner/Platform/MessagesImpl.swift b/mobile/ios/Runner/Platform/MessagesImpl.swift index 461df1217f..92dc60e326 100644 --- a/mobile/ios/Runner/Platform/MessagesImpl.swift +++ b/mobile/ios/Runner/Platform/MessagesImpl.swift @@ -41,4 +41,8 @@ class ImHostServiceImpl: ImHostService { } } + func getAssetIdsForAlbum(albumId: String, completion: @escaping (Result<[String], any Error>) -> Void) { + // Android specific, ignore the call with an empty list + completion(.success([])) + } } diff --git a/mobile/lib/domain/interfaces/local_album.interface.dart b/mobile/lib/domain/interfaces/local_album.interface.dart index b5a6924fa9..616e71dc4f 100644 --- a/mobile/lib/domain/interfaces/local_album.interface.dart +++ b/mobile/lib/domain/interfaces/local_album.interface.dart @@ -12,6 +12,8 @@ abstract interface class ILocalAlbumRepository implements IDatabaseRepository { Future> getAssetsForAlbum(String albumId); + Future> getAssetIdsForAlbum(String albumId); + Future update(LocalAlbum album); Future updateAll(Iterable albums); @@ -20,6 +22,8 @@ abstract interface class ILocalAlbumRepository implements IDatabaseRepository { Future delete(String albumId); + Future removeMissing(String albumId, Iterable assetIds); + Future removeAssets(String albumId, Iterable assetIds); } diff --git a/mobile/lib/domain/services/device_sync.service.dart b/mobile/lib/domain/services/device_sync.service.dart index f6160df335..08b5440daa 100644 --- a/mobile/lib/domain/services/device_sync.service.dart +++ b/mobile/lib/domain/services/device_sync.service.dart @@ -9,10 +9,12 @@ import 'package:immich_mobile/domain/models/local_album.model.dart'; import 'package:immich_mobile/platform/messages.g.dart' as platform; import 'package:immich_mobile/utils/diff.dart'; import 'package:logging/logging.dart'; +import 'package:platform/platform.dart'; class DeviceSyncService { final IAlbumMediaRepository _albumMediaRepository; final ILocalAlbumRepository _localAlbumRepository; + final Platform _platform; final platform.ImHostService _hostService; final Logger _log = Logger("SyncService"); @@ -20,8 +22,10 @@ class DeviceSyncService { required IAlbumMediaRepository albumMediaRepository, required ILocalAlbumRepository localAlbumRepository, required platform.ImHostService hostService, + Platform? platform, }) : _albumMediaRepository = albumMediaRepository, _localAlbumRepository = localAlbumRepository, + _platform = platform ?? const LocalPlatform(), _hostService = hostService; Future sync() async { @@ -41,6 +45,15 @@ class DeviceSyncService { final delta = await _hostService.getMediaChanges(); await _localAlbumRepository.handleSyncDelta(delta); + + if (_platform.isAndroid) { + final dbAlbums = await _localAlbumRepository.getAll(); + for (final album in dbAlbums) { + final deviceIds = await _hostService.getAssetIdsForAlbum(album.id); + await _localAlbumRepository.removeMissing(album.id, deviceIds); + } + } + await _hostService.checkpointSync(); } catch (e, s) { _log.severe("Error performing device sync", e, s); @@ -69,7 +82,7 @@ class DeviceSyncService { onlySecond: addAlbum, ); - _hostService.checkpointSync(); + await _hostService.checkpointSync(); stopwatch.stop(); _log.info("Full device sync took - ${stopwatch.elapsedMilliseconds}ms"); } catch (e, s) { diff --git a/mobile/lib/infrastructure/entities/exif.entity.dart b/mobile/lib/infrastructure/entities/exif.entity.dart index bb3f2d177c..da5261eff1 100644 --- a/mobile/lib/infrastructure/entities/exif.entity.dart +++ b/mobile/lib/infrastructure/entities/exif.entity.dart @@ -97,8 +97,8 @@ class ExifInfo { class ExifEntity extends Table with DriftDefaultsMixin { const ExifEntity(); - BlobColumn get assetId => blob() - .references(RemoteAssetEntity, #remoteId, onDelete: KeyAction.cascade)(); + BlobColumn get assetId => + blob().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)(); TextColumn get city => text().nullable()(); diff --git a/mobile/lib/infrastructure/entities/exif.entity.drift.dart b/mobile/lib/infrastructure/entities/exif.entity.drift.dart index 75ec61d3c7..8635d678a6 100644 --- a/mobile/lib/infrastructure/entities/exif.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/exif.entity.drift.dart @@ -71,7 +71,7 @@ final class $$ExifEntityTableReferences extends i0.BaseReferences< .assetId, i5.ReadDatabaseContainer(db) .resultSet('remote_asset_entity') - .remoteId)); + .id)); i4.$$RemoteAssetEntityTableProcessedTableManager get assetId { final $_column = $_itemColumn('asset_id')!; @@ -81,7 +81,7 @@ final class $$ExifEntityTableReferences extends i0.BaseReferences< $_db, i5.ReadDatabaseContainer($_db) .resultSet('remote_asset_entity')) - .filter((f) => f.remoteId.sqlEquals($_column)); + .filter((f) => f.id.sqlEquals($_column)); final item = $_typedResult.readTableOrNull(_assetIdTable($_db)); if (item == null) return manager; return i0.ProcessedTableManager( @@ -170,7 +170,7 @@ class $$ExifEntityTableFilterComposer getCurrentColumn: (t) => t.assetId, referencedTable: i5.ReadDatabaseContainer($db) .resultSet('remote_asset_entity'), - getReferencedColumn: (t) => t.remoteId, + getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => @@ -270,7 +270,7 @@ class $$ExifEntityTableOrderingComposer getCurrentColumn: (t) => t.assetId, referencedTable: i5.ReadDatabaseContainer($db) .resultSet('remote_asset_entity'), - getReferencedColumn: (t) => t.remoteId, + getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => @@ -364,7 +364,7 @@ class $$ExifEntityTableAnnotationComposer getCurrentColumn: (t) => t.assetId, referencedTable: i5.ReadDatabaseContainer($db) .resultSet('remote_asset_entity'), - getReferencedColumn: (t) => t.remoteId, + getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => @@ -526,9 +526,8 @@ class $$ExifEntityTableTableManager extends i0.RootTableManager< currentColumn: table.assetId, referencedTable: i1.$$ExifEntityTableReferences._assetIdTable(db), - referencedColumn: i1.$$ExifEntityTableReferences - ._assetIdTable(db) - .remoteId, + referencedColumn: + i1.$$ExifEntityTableReferences._assetIdTable(db).id, ) as T; } @@ -569,7 +568,7 @@ class $ExifEntityTable extends i3.ExifEntity type: i0.DriftSqlType.blob, requiredDuringInsert: true, defaultConstraints: i0.GeneratedColumn.constraintIsAlways( - 'REFERENCES remote_asset_entity (remote_id) ON DELETE CASCADE')); + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); static const i0.VerificationMeta _cityMeta = const i0.VerificationMeta('city'); @override diff --git a/mobile/lib/infrastructure/entities/local_album.entity.dart b/mobile/lib/infrastructure/entities/local_album.entity.dart index d6b10da819..d91d950a39 100644 --- a/mobile/lib/infrastructure/entities/local_album.entity.dart +++ b/mobile/lib/infrastructure/entities/local_album.entity.dart @@ -10,6 +10,8 @@ class LocalAlbumEntity extends Table with DriftDefaultsMixin { TextColumn get name => text()(); DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); IntColumn get backupSelection => intEnum()(); + + // Used for mark & sweep BoolColumn get marker_ => boolean().withDefault(const Constant(false))(); @override diff --git a/mobile/lib/infrastructure/entities/local_album_asset.entity.dart b/mobile/lib/infrastructure/entities/local_album_asset.entity.dart index f1c3582b5a..b64b9ec2fb 100644 --- a/mobile/lib/infrastructure/entities/local_album_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/local_album_asset.entity.dart @@ -6,8 +6,8 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; class LocalAlbumAssetEntity extends Table with DriftDefaultsMixin { const LocalAlbumAssetEntity(); - TextColumn get assetId => text() - .references(LocalAssetEntity, #localId, onDelete: KeyAction.cascade)(); + TextColumn get assetId => + text().references(LocalAssetEntity, #id, onDelete: KeyAction.cascade)(); TextColumn get albumId => text().references(LocalAlbumEntity, #id, onDelete: KeyAction.cascade)(); diff --git a/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart index 86264ef75e..e8f94fa74b 100644 --- a/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/local_album_asset.entity.drift.dart @@ -39,7 +39,7 @@ final class $$LocalAlbumAssetEntityTableReferences extends i0.BaseReferences< .assetId, i4.ReadDatabaseContainer(db) .resultSet('local_asset_entity') - .localId)); + .id)); i3.$$LocalAssetEntityTableProcessedTableManager get assetId { final $_column = $_itemColumn('asset_id')!; @@ -49,7 +49,7 @@ final class $$LocalAlbumAssetEntityTableReferences extends i0.BaseReferences< $_db, i4.ReadDatabaseContainer($_db) .resultSet('local_asset_entity')) - .filter((f) => f.localId.sqlEquals($_column)); + .filter((f) => f.id.sqlEquals($_column)); final item = $_typedResult.readTableOrNull(_assetIdTable($_db)); if (item == null) return manager; return i0.ProcessedTableManager( @@ -99,7 +99,7 @@ class $$LocalAlbumAssetEntityTableFilterComposer getCurrentColumn: (t) => t.assetId, referencedTable: i4.ReadDatabaseContainer($db) .resultSet('local_asset_entity'), - getReferencedColumn: (t) => t.localId, + getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => @@ -154,7 +154,7 @@ class $$LocalAlbumAssetEntityTableOrderingComposer getCurrentColumn: (t) => t.assetId, referencedTable: i4.ReadDatabaseContainer($db) .resultSet('local_asset_entity'), - getReferencedColumn: (t) => t.localId, + getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => @@ -212,7 +212,7 @@ class $$LocalAlbumAssetEntityTableAnnotationComposer getCurrentColumn: (t) => t.assetId, referencedTable: i4.ReadDatabaseContainer($db) .resultSet('local_asset_entity'), - getReferencedColumn: (t) => t.localId, + getReferencedColumn: (t) => t.id, builder: (joinBuilder, {$addJoinBuilderToRootComposer, $removeJoinBuilderFromRootComposer}) => @@ -327,7 +327,7 @@ class $$LocalAlbumAssetEntityTableTableManager extends i0.RootTableManager< ._assetIdTable(db), referencedColumn: i1.$$LocalAlbumAssetEntityTableReferences ._assetIdTable(db) - .localId, + .id, ) as T; } if (albumId) { @@ -385,7 +385,7 @@ class $LocalAlbumAssetEntityTable extends i2.LocalAlbumAssetEntity type: i0.DriftSqlType.string, requiredDuringInsert: true, defaultConstraints: i0.GeneratedColumn.constraintIsAlways( - 'REFERENCES local_asset_entity (local_id) ON DELETE CASCADE')); + 'REFERENCES local_asset_entity (id) ON DELETE CASCADE')); static const i0.VerificationMeta _albumIdMeta = const i0.VerificationMeta('albumId'); @override diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.dart b/mobile/lib/infrastructure/entities/local_asset.entity.dart index 2127ab1713..3d578988e1 100644 --- a/mobile/lib/infrastructure/entities/local_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/local_asset.entity.dart @@ -8,7 +8,7 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin { const LocalAssetEntity(); - TextColumn get localId => text()(); + TextColumn get id => text()(); TextColumn get checksum => text().nullable()(); @@ -16,13 +16,13 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin { BoolColumn get isFavorite => boolean().withDefault(const Constant(false))(); @override - Set get primaryKey => {localId}; + Set get primaryKey => {id}; } extension LocalAssetEntityX on LocalAssetEntityData { LocalAsset toDto() { return LocalAsset( - id: localId, + id: id, name: name, checksum: checksum, type: type, diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart index e6a567d033..0a4896a4a3 100644 --- a/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart @@ -15,7 +15,7 @@ typedef $$LocalAssetEntityTableCreateCompanionBuilder i0.Value createdAt, i0.Value updatedAt, i0.Value durationInSeconds, - required String localId, + required String id, i0.Value checksum, i0.Value isFavorite, }); @@ -26,7 +26,7 @@ typedef $$LocalAssetEntityTableUpdateCompanionBuilder i0.Value createdAt, i0.Value updatedAt, i0.Value durationInSeconds, - i0.Value localId, + i0.Value id, i0.Value checksum, i0.Value isFavorite, }); @@ -58,8 +58,8 @@ class $$LocalAssetEntityTableFilterComposer column: $table.durationInSeconds, builder: (column) => i0.ColumnFilters(column)); - i0.ColumnFilters get localId => $composableBuilder( - column: $table.localId, builder: (column) => i0.ColumnFilters(column)); + i0.ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnFilters(column)); i0.ColumnFilters get checksum => $composableBuilder( column: $table.checksum, builder: (column) => i0.ColumnFilters(column)); @@ -95,8 +95,8 @@ class $$LocalAssetEntityTableOrderingComposer column: $table.durationInSeconds, builder: (column) => i0.ColumnOrderings(column)); - i0.ColumnOrderings get localId => $composableBuilder( - column: $table.localId, builder: (column) => i0.ColumnOrderings(column)); + i0.ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnOrderings(column)); i0.ColumnOrderings get checksum => $composableBuilder( column: $table.checksum, builder: (column) => i0.ColumnOrderings(column)); @@ -130,8 +130,8 @@ class $$LocalAssetEntityTableAnnotationComposer i0.GeneratedColumn get durationInSeconds => $composableBuilder( column: $table.durationInSeconds, builder: (column) => column); - i0.GeneratedColumn get localId => - $composableBuilder(column: $table.localId, builder: (column) => column); + i0.GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); i0.GeneratedColumn get checksum => $composableBuilder(column: $table.checksum, builder: (column) => column); @@ -174,7 +174,7 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager< i0.Value createdAt = const i0.Value.absent(), i0.Value updatedAt = const i0.Value.absent(), i0.Value durationInSeconds = const i0.Value.absent(), - i0.Value localId = const i0.Value.absent(), + i0.Value id = const i0.Value.absent(), i0.Value checksum = const i0.Value.absent(), i0.Value isFavorite = const i0.Value.absent(), }) => @@ -184,7 +184,7 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager< createdAt: createdAt, updatedAt: updatedAt, durationInSeconds: durationInSeconds, - localId: localId, + id: id, checksum: checksum, isFavorite: isFavorite, ), @@ -194,7 +194,7 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager< i0.Value createdAt = const i0.Value.absent(), i0.Value updatedAt = const i0.Value.absent(), i0.Value durationInSeconds = const i0.Value.absent(), - required String localId, + required String id, i0.Value checksum = const i0.Value.absent(), i0.Value isFavorite = const i0.Value.absent(), }) => @@ -204,7 +204,7 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager< createdAt: createdAt, updatedAt: updatedAt, durationInSeconds: durationInSeconds, - localId: localId, + id: id, checksum: checksum, isFavorite: isFavorite, ), @@ -274,11 +274,10 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity late final i0.GeneratedColumn durationInSeconds = i0.GeneratedColumn('duration_in_seconds', aliasedName, true, type: i0.DriftSqlType.int, requiredDuringInsert: false); - static const i0.VerificationMeta _localIdMeta = - const i0.VerificationMeta('localId'); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); @override - late final i0.GeneratedColumn localId = i0.GeneratedColumn( - 'local_id', aliasedName, false, + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, type: i0.DriftSqlType.string, requiredDuringInsert: true); static const i0.VerificationMeta _checksumMeta = const i0.VerificationMeta('checksum'); @@ -303,7 +302,7 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity createdAt, updatedAt, durationInSeconds, - localId, + id, checksum, isFavorite ]; @@ -338,11 +337,10 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity durationInSeconds.isAcceptableOrUnknown( data['duration_in_seconds']!, _durationInSecondsMeta)); } - if (data.containsKey('local_id')) { - context.handle(_localIdMeta, - localId.isAcceptableOrUnknown(data['local_id']!, _localIdMeta)); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); } else if (isInserting) { - context.missing(_localIdMeta); + context.missing(_idMeta); } if (data.containsKey('checksum')) { context.handle(_checksumMeta, @@ -358,7 +356,7 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity } @override - Set get $primaryKey => {localId}; + Set get $primaryKey => {id}; @override i1.LocalAssetEntityData map(Map data, {String? tablePrefix}) { @@ -375,8 +373,8 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, durationInSeconds: attachedDatabase.typeMapping.read( i0.DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), - localId: attachedDatabase.typeMapping - .read(i0.DriftSqlType.string, data['${effectivePrefix}local_id'])!, + id: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, checksum: attachedDatabase.typeMapping .read(i0.DriftSqlType.string, data['${effectivePrefix}checksum']), isFavorite: attachedDatabase.typeMapping @@ -404,7 +402,7 @@ class LocalAssetEntityData extends i0.DataClass final DateTime createdAt; final DateTime updatedAt; final int? durationInSeconds; - final String localId; + final String id; final String? checksum; final bool isFavorite; const LocalAssetEntityData( @@ -413,7 +411,7 @@ class LocalAssetEntityData extends i0.DataClass required this.createdAt, required this.updatedAt, this.durationInSeconds, - required this.localId, + required this.id, this.checksum, required this.isFavorite}); @override @@ -429,7 +427,7 @@ class LocalAssetEntityData extends i0.DataClass if (!nullToAbsent || durationInSeconds != null) { map['duration_in_seconds'] = i0.Variable(durationInSeconds); } - map['local_id'] = i0.Variable(localId); + map['id'] = i0.Variable(id); if (!nullToAbsent || checksum != null) { map['checksum'] = i0.Variable(checksum); } @@ -447,7 +445,7 @@ class LocalAssetEntityData extends i0.DataClass createdAt: serializer.fromJson(json['createdAt']), updatedAt: serializer.fromJson(json['updatedAt']), durationInSeconds: serializer.fromJson(json['durationInSeconds']), - localId: serializer.fromJson(json['localId']), + id: serializer.fromJson(json['id']), checksum: serializer.fromJson(json['checksum']), isFavorite: serializer.fromJson(json['isFavorite']), ); @@ -462,7 +460,7 @@ class LocalAssetEntityData extends i0.DataClass 'createdAt': serializer.toJson(createdAt), 'updatedAt': serializer.toJson(updatedAt), 'durationInSeconds': serializer.toJson(durationInSeconds), - 'localId': serializer.toJson(localId), + 'id': serializer.toJson(id), 'checksum': serializer.toJson(checksum), 'isFavorite': serializer.toJson(isFavorite), }; @@ -474,7 +472,7 @@ class LocalAssetEntityData extends i0.DataClass DateTime? createdAt, DateTime? updatedAt, i0.Value durationInSeconds = const i0.Value.absent(), - String? localId, + String? id, i0.Value checksum = const i0.Value.absent(), bool? isFavorite}) => i1.LocalAssetEntityData( @@ -485,7 +483,7 @@ class LocalAssetEntityData extends i0.DataClass durationInSeconds: durationInSeconds.present ? durationInSeconds.value : this.durationInSeconds, - localId: localId ?? this.localId, + id: id ?? this.id, checksum: checksum.present ? checksum.value : this.checksum, isFavorite: isFavorite ?? this.isFavorite, ); @@ -498,7 +496,7 @@ class LocalAssetEntityData extends i0.DataClass durationInSeconds: data.durationInSeconds.present ? data.durationInSeconds.value : this.durationInSeconds, - localId: data.localId.present ? data.localId.value : this.localId, + 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, @@ -513,7 +511,7 @@ class LocalAssetEntityData extends i0.DataClass ..write('createdAt: $createdAt, ') ..write('updatedAt: $updatedAt, ') ..write('durationInSeconds: $durationInSeconds, ') - ..write('localId: $localId, ') + ..write('id: $id, ') ..write('checksum: $checksum, ') ..write('isFavorite: $isFavorite') ..write(')')) @@ -522,7 +520,7 @@ class LocalAssetEntityData extends i0.DataClass @override int get hashCode => Object.hash(name, type, createdAt, updatedAt, - durationInSeconds, localId, checksum, isFavorite); + durationInSeconds, id, checksum, isFavorite); @override bool operator ==(Object other) => identical(this, other) || @@ -532,7 +530,7 @@ class LocalAssetEntityData extends i0.DataClass other.createdAt == this.createdAt && other.updatedAt == this.updatedAt && other.durationInSeconds == this.durationInSeconds && - other.localId == this.localId && + other.id == this.id && other.checksum == this.checksum && other.isFavorite == this.isFavorite); } @@ -544,7 +542,7 @@ class LocalAssetEntityCompanion final i0.Value createdAt; final i0.Value updatedAt; final i0.Value durationInSeconds; - final i0.Value localId; + final i0.Value id; final i0.Value checksum; final i0.Value isFavorite; const LocalAssetEntityCompanion({ @@ -553,7 +551,7 @@ class LocalAssetEntityCompanion this.createdAt = const i0.Value.absent(), this.updatedAt = const i0.Value.absent(), this.durationInSeconds = const i0.Value.absent(), - this.localId = const i0.Value.absent(), + this.id = const i0.Value.absent(), this.checksum = const i0.Value.absent(), this.isFavorite = const i0.Value.absent(), }); @@ -563,19 +561,19 @@ class LocalAssetEntityCompanion this.createdAt = const i0.Value.absent(), this.updatedAt = const i0.Value.absent(), this.durationInSeconds = const i0.Value.absent(), - required String localId, + required String id, this.checksum = const i0.Value.absent(), this.isFavorite = const i0.Value.absent(), }) : name = i0.Value(name), type = i0.Value(type), - localId = i0.Value(localId); + id = i0.Value(id); static i0.Insertable custom({ i0.Expression? name, i0.Expression? type, i0.Expression? createdAt, i0.Expression? updatedAt, i0.Expression? durationInSeconds, - i0.Expression? localId, + i0.Expression? id, i0.Expression? checksum, i0.Expression? isFavorite, }) { @@ -585,7 +583,7 @@ class LocalAssetEntityCompanion if (createdAt != null) 'created_at': createdAt, if (updatedAt != null) 'updated_at': updatedAt, if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, - if (localId != null) 'local_id': localId, + if (id != null) 'id': id, if (checksum != null) 'checksum': checksum, if (isFavorite != null) 'is_favorite': isFavorite, }); @@ -597,7 +595,7 @@ class LocalAssetEntityCompanion i0.Value? createdAt, i0.Value? updatedAt, i0.Value? durationInSeconds, - i0.Value? localId, + i0.Value? id, i0.Value? checksum, i0.Value? isFavorite}) { return i1.LocalAssetEntityCompanion( @@ -606,7 +604,7 @@ class LocalAssetEntityCompanion createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, durationInSeconds: durationInSeconds ?? this.durationInSeconds, - localId: localId ?? this.localId, + id: id ?? this.id, checksum: checksum ?? this.checksum, isFavorite: isFavorite ?? this.isFavorite, ); @@ -631,8 +629,8 @@ class LocalAssetEntityCompanion if (durationInSeconds.present) { map['duration_in_seconds'] = i0.Variable(durationInSeconds.value); } - if (localId.present) { - map['local_id'] = i0.Variable(localId.value); + if (id.present) { + map['id'] = i0.Variable(id.value); } if (checksum.present) { map['checksum'] = i0.Variable(checksum.value); @@ -651,7 +649,7 @@ class LocalAssetEntityCompanion ..write('createdAt: $createdAt, ') ..write('updatedAt: $updatedAt, ') ..write('durationInSeconds: $durationInSeconds, ') - ..write('localId: $localId, ') + ..write('id: $id, ') ..write('checksum: $checksum, ') ..write('isFavorite: $isFavorite') ..write(')')) diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.dart index 8036ea18b5..01a51fa455 100644 --- a/mobile/lib/infrastructure/entities/remote_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.dart @@ -8,7 +8,7 @@ class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin { const RemoteAssetEntity(); - BlobColumn get remoteId => blob()(); + BlobColumn get id => blob()(); TextColumn get checksum => text().unique()(); @@ -24,5 +24,5 @@ class RemoteAssetEntity extends Table DateTimeColumn get deletedAt => dateTime().nullable()(); @override - Set get primaryKey => {remoteId}; + Set get primaryKey => {id}; } diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart index b868709c5b..83b679af61 100644 --- a/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart @@ -19,7 +19,7 @@ typedef $$RemoteAssetEntityTableCreateCompanionBuilder i0.Value createdAt, i0.Value updatedAt, i0.Value durationInSeconds, - required i3.Uint8List remoteId, + required i3.Uint8List id, required String checksum, i0.Value isFavorite, required i3.Uint8List ownerId, @@ -34,7 +34,7 @@ typedef $$RemoteAssetEntityTableUpdateCompanionBuilder i0.Value createdAt, i0.Value updatedAt, i0.Value durationInSeconds, - i0.Value remoteId, + i0.Value id, i0.Value checksum, i0.Value isFavorite, i0.Value ownerId, @@ -104,8 +104,8 @@ class $$RemoteAssetEntityTableFilterComposer column: $table.durationInSeconds, builder: (column) => i0.ColumnFilters(column)); - i0.ColumnFilters get remoteId => $composableBuilder( - column: $table.remoteId, builder: (column) => i0.ColumnFilters(column)); + i0.ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnFilters(column)); i0.ColumnFilters get checksum => $composableBuilder( column: $table.checksum, builder: (column) => i0.ColumnFilters(column)); @@ -173,8 +173,8 @@ class $$RemoteAssetEntityTableOrderingComposer column: $table.durationInSeconds, builder: (column) => i0.ColumnOrderings(column)); - i0.ColumnOrderings get remoteId => $composableBuilder( - column: $table.remoteId, builder: (column) => i0.ColumnOrderings(column)); + i0.ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnOrderings(column)); i0.ColumnOrderings get checksum => $composableBuilder( column: $table.checksum, builder: (column) => i0.ColumnOrderings(column)); @@ -242,8 +242,8 @@ class $$RemoteAssetEntityTableAnnotationComposer i0.GeneratedColumn get durationInSeconds => $composableBuilder( column: $table.durationInSeconds, builder: (column) => column); - i0.GeneratedColumn get remoteId => - $composableBuilder(column: $table.remoteId, builder: (column) => column); + i0.GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); i0.GeneratedColumn get checksum => $composableBuilder(column: $table.checksum, builder: (column) => column); @@ -313,7 +313,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager< i0.Value createdAt = const i0.Value.absent(), i0.Value updatedAt = const i0.Value.absent(), i0.Value durationInSeconds = const i0.Value.absent(), - i0.Value remoteId = const i0.Value.absent(), + i0.Value id = const i0.Value.absent(), i0.Value checksum = const i0.Value.absent(), i0.Value isFavorite = const i0.Value.absent(), i0.Value ownerId = const i0.Value.absent(), @@ -327,7 +327,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager< createdAt: createdAt, updatedAt: updatedAt, durationInSeconds: durationInSeconds, - remoteId: remoteId, + id: id, checksum: checksum, isFavorite: isFavorite, ownerId: ownerId, @@ -341,7 +341,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager< i0.Value createdAt = const i0.Value.absent(), i0.Value updatedAt = const i0.Value.absent(), i0.Value durationInSeconds = const i0.Value.absent(), - required i3.Uint8List remoteId, + required i3.Uint8List id, required String checksum, i0.Value isFavorite = const i0.Value.absent(), required i3.Uint8List ownerId, @@ -355,7 +355,7 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager< createdAt: createdAt, updatedAt: updatedAt, durationInSeconds: durationInSeconds, - remoteId: remoteId, + id: id, checksum: checksum, isFavorite: isFavorite, ownerId: ownerId, @@ -464,11 +464,10 @@ class $RemoteAssetEntityTable extends i4.RemoteAssetEntity late final i0.GeneratedColumn durationInSeconds = i0.GeneratedColumn('duration_in_seconds', aliasedName, true, type: i0.DriftSqlType.int, requiredDuringInsert: false); - static const i0.VerificationMeta _remoteIdMeta = - const i0.VerificationMeta('remoteId'); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); @override - late final i0.GeneratedColumn remoteId = - i0.GeneratedColumn('remote_id', aliasedName, false, + late final i0.GeneratedColumn id = + i0.GeneratedColumn('id', aliasedName, false, type: i0.DriftSqlType.blob, requiredDuringInsert: true); static const i0.VerificationMeta _checksumMeta = const i0.VerificationMeta('checksum'); @@ -522,7 +521,7 @@ class $RemoteAssetEntityTable extends i4.RemoteAssetEntity createdAt, updatedAt, durationInSeconds, - remoteId, + id, checksum, isFavorite, ownerId, @@ -561,11 +560,10 @@ class $RemoteAssetEntityTable extends i4.RemoteAssetEntity durationInSeconds.isAcceptableOrUnknown( data['duration_in_seconds']!, _durationInSecondsMeta)); } - if (data.containsKey('remote_id')) { - context.handle(_remoteIdMeta, - remoteId.isAcceptableOrUnknown(data['remote_id']!, _remoteIdMeta)); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); } else if (isInserting) { - context.missing(_remoteIdMeta); + context.missing(_idMeta); } if (data.containsKey('checksum')) { context.handle(_checksumMeta, @@ -603,7 +601,7 @@ class $RemoteAssetEntityTable extends i4.RemoteAssetEntity } @override - Set get $primaryKey => {remoteId}; + Set get $primaryKey => {id}; @override i1.RemoteAssetEntityData map(Map data, {String? tablePrefix}) { @@ -620,8 +618,8 @@ class $RemoteAssetEntityTable extends i4.RemoteAssetEntity i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, durationInSeconds: attachedDatabase.typeMapping.read( i0.DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), - remoteId: attachedDatabase.typeMapping - .read(i0.DriftSqlType.blob, data['${effectivePrefix}remote_id'])!, + id: attachedDatabase.typeMapping + .read(i0.DriftSqlType.blob, data['${effectivePrefix}id'])!, checksum: attachedDatabase.typeMapping .read(i0.DriftSqlType.string, data['${effectivePrefix}checksum'])!, isFavorite: attachedDatabase.typeMapping @@ -657,7 +655,7 @@ class RemoteAssetEntityData extends i0.DataClass final DateTime createdAt; final DateTime updatedAt; final int? durationInSeconds; - final i3.Uint8List remoteId; + final i3.Uint8List id; final String checksum; final bool isFavorite; final i3.Uint8List ownerId; @@ -670,7 +668,7 @@ class RemoteAssetEntityData extends i0.DataClass required this.createdAt, required this.updatedAt, this.durationInSeconds, - required this.remoteId, + required this.id, required this.checksum, required this.isFavorite, required this.ownerId, @@ -690,7 +688,7 @@ class RemoteAssetEntityData extends i0.DataClass if (!nullToAbsent || durationInSeconds != null) { map['duration_in_seconds'] = i0.Variable(durationInSeconds); } - map['remote_id'] = i0.Variable(remoteId); + map['id'] = i0.Variable(id); map['checksum'] = i0.Variable(checksum); map['is_favorite'] = i0.Variable(isFavorite); map['owner_id'] = i0.Variable(ownerId); @@ -716,7 +714,7 @@ class RemoteAssetEntityData extends i0.DataClass createdAt: serializer.fromJson(json['createdAt']), updatedAt: serializer.fromJson(json['updatedAt']), durationInSeconds: serializer.fromJson(json['durationInSeconds']), - remoteId: serializer.fromJson(json['remoteId']), + id: serializer.fromJson(json['id']), checksum: serializer.fromJson(json['checksum']), isFavorite: serializer.fromJson(json['isFavorite']), ownerId: serializer.fromJson(json['ownerId']), @@ -735,7 +733,7 @@ class RemoteAssetEntityData extends i0.DataClass 'createdAt': serializer.toJson(createdAt), 'updatedAt': serializer.toJson(updatedAt), 'durationInSeconds': serializer.toJson(durationInSeconds), - 'remoteId': serializer.toJson(remoteId), + 'id': serializer.toJson(id), 'checksum': serializer.toJson(checksum), 'isFavorite': serializer.toJson(isFavorite), 'ownerId': serializer.toJson(ownerId), @@ -751,7 +749,7 @@ class RemoteAssetEntityData extends i0.DataClass DateTime? createdAt, DateTime? updatedAt, i0.Value durationInSeconds = const i0.Value.absent(), - i3.Uint8List? remoteId, + i3.Uint8List? id, String? checksum, bool? isFavorite, i3.Uint8List? ownerId, @@ -766,7 +764,7 @@ class RemoteAssetEntityData extends i0.DataClass durationInSeconds: durationInSeconds.present ? durationInSeconds.value : this.durationInSeconds, - remoteId: remoteId ?? this.remoteId, + id: id ?? this.id, checksum: checksum ?? this.checksum, isFavorite: isFavorite ?? this.isFavorite, ownerId: ownerId ?? this.ownerId, @@ -784,7 +782,7 @@ class RemoteAssetEntityData extends i0.DataClass durationInSeconds: data.durationInSeconds.present ? data.durationInSeconds.value : this.durationInSeconds, - remoteId: data.remoteId.present ? data.remoteId.value : this.remoteId, + 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, @@ -805,7 +803,7 @@ class RemoteAssetEntityData extends i0.DataClass ..write('createdAt: $createdAt, ') ..write('updatedAt: $updatedAt, ') ..write('durationInSeconds: $durationInSeconds, ') - ..write('remoteId: $remoteId, ') + ..write('id: $id, ') ..write('checksum: $checksum, ') ..write('isFavorite: $isFavorite, ') ..write('ownerId: $ownerId, ') @@ -823,7 +821,7 @@ class RemoteAssetEntityData extends i0.DataClass createdAt, updatedAt, durationInSeconds, - i0.$driftBlobEquality.hash(remoteId), + i0.$driftBlobEquality.hash(id), checksum, isFavorite, i0.$driftBlobEquality.hash(ownerId), @@ -839,7 +837,7 @@ class RemoteAssetEntityData extends i0.DataClass other.createdAt == this.createdAt && other.updatedAt == this.updatedAt && other.durationInSeconds == this.durationInSeconds && - i0.$driftBlobEquality.equals(other.remoteId, this.remoteId) && + i0.$driftBlobEquality.equals(other.id, this.id) && other.checksum == this.checksum && other.isFavorite == this.isFavorite && i0.$driftBlobEquality.equals(other.ownerId, this.ownerId) && @@ -855,7 +853,7 @@ class RemoteAssetEntityCompanion final i0.Value createdAt; final i0.Value updatedAt; final i0.Value durationInSeconds; - final i0.Value remoteId; + final i0.Value id; final i0.Value checksum; final i0.Value isFavorite; final i0.Value ownerId; @@ -868,7 +866,7 @@ class RemoteAssetEntityCompanion this.createdAt = const i0.Value.absent(), this.updatedAt = const i0.Value.absent(), this.durationInSeconds = const i0.Value.absent(), - this.remoteId = const i0.Value.absent(), + this.id = const i0.Value.absent(), this.checksum = const i0.Value.absent(), this.isFavorite = const i0.Value.absent(), this.ownerId = const i0.Value.absent(), @@ -882,7 +880,7 @@ class RemoteAssetEntityCompanion this.createdAt = const i0.Value.absent(), this.updatedAt = const i0.Value.absent(), this.durationInSeconds = const i0.Value.absent(), - required i3.Uint8List remoteId, + required i3.Uint8List id, required String checksum, this.isFavorite = const i0.Value.absent(), required i3.Uint8List ownerId, @@ -891,7 +889,7 @@ class RemoteAssetEntityCompanion this.deletedAt = const i0.Value.absent(), }) : name = i0.Value(name), type = i0.Value(type), - remoteId = i0.Value(remoteId), + id = i0.Value(id), checksum = i0.Value(checksum), ownerId = i0.Value(ownerId); static i0.Insertable custom({ @@ -900,7 +898,7 @@ class RemoteAssetEntityCompanion i0.Expression? createdAt, i0.Expression? updatedAt, i0.Expression? durationInSeconds, - i0.Expression? remoteId, + i0.Expression? id, i0.Expression? checksum, i0.Expression? isFavorite, i0.Expression? ownerId, @@ -914,7 +912,7 @@ class RemoteAssetEntityCompanion if (createdAt != null) 'created_at': createdAt, if (updatedAt != null) 'updated_at': updatedAt, if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, - if (remoteId != null) 'remote_id': remoteId, + if (id != null) 'id': id, if (checksum != null) 'checksum': checksum, if (isFavorite != null) 'is_favorite': isFavorite, if (ownerId != null) 'owner_id': ownerId, @@ -930,7 +928,7 @@ class RemoteAssetEntityCompanion i0.Value? createdAt, i0.Value? updatedAt, i0.Value? durationInSeconds, - i0.Value? remoteId, + i0.Value? id, i0.Value? checksum, i0.Value? isFavorite, i0.Value? ownerId, @@ -943,7 +941,7 @@ class RemoteAssetEntityCompanion createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, durationInSeconds: durationInSeconds ?? this.durationInSeconds, - remoteId: remoteId ?? this.remoteId, + id: id ?? this.id, checksum: checksum ?? this.checksum, isFavorite: isFavorite ?? this.isFavorite, ownerId: ownerId ?? this.ownerId, @@ -972,8 +970,8 @@ class RemoteAssetEntityCompanion if (durationInSeconds.present) { map['duration_in_seconds'] = i0.Variable(durationInSeconds.value); } - if (remoteId.present) { - map['remote_id'] = i0.Variable(remoteId.value); + if (id.present) { + map['id'] = i0.Variable(id.value); } if (checksum.present) { map['checksum'] = i0.Variable(checksum.value); @@ -1004,7 +1002,7 @@ class RemoteAssetEntityCompanion ..write('createdAt: $createdAt, ') ..write('updatedAt: $updatedAt, ') ..write('durationInSeconds: $durationInSeconds, ') - ..write('remoteId: $remoteId, ') + ..write('id: $id, ') ..write('checksum: $checksum, ') ..write('isFavorite: $isFavorite, ') ..write('ownerId: $ownerId, ') diff --git a/mobile/lib/infrastructure/repositories/local_album.repository.dart b/mobile/lib/infrastructure/repositories/local_album.repository.dart index 12d3066de9..21acb4556d 100644 --- a/mobile/lib/infrastructure/repositories/local_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_album.repository.dart @@ -81,6 +81,31 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository }); } + @override + Future removeMissing(String albumId, Iterable assetIds) async { + if (assetIds.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(assetIds), + ); + return localAsset.id.isInQuery(subQuery); + }); + } + @override Future removeAssets(String albumId, Iterable assetIds) async { if (assetIds.isEmpty) { @@ -130,7 +155,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository return _db.transaction(() async { await _db.localAlbumEntity .update() - .write(const LocalAlbumEntityCompanion(marker_: Value(false))); + .write(const LocalAlbumEntityCompanion(marker_: Value(true))); await _db.batch((batch) { for (final album in albums) { @@ -150,6 +175,26 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository } }); + 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_.equals(false)); + return localAsset.id.isInQuery(subQuery); + }); + } + await _db.localAlbumEntity.deleteWhere((f) => f.marker_.equals(false)); }); } @@ -160,30 +205,41 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository [ innerJoin( _db.localAssetEntity, - _db.localAlbumAssetEntity.assetId - .equalsExp(_db.localAssetEntity.localId), + _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), ), ], ) ..where(_db.localAlbumAssetEntity.albumId.equals(albumId)) - ..orderBy([OrderingTerm.desc(_db.localAssetEntity.localId)]); + ..orderBy([OrderingTerm.desc(_db.localAssetEntity.id)]); return query .map((row) => row.readTable(_db.localAssetEntity).toDto()) .get(); } + @override + Future> 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 handleSyncDelta(platform.SyncDelta delta) { return _db.transaction(() async { await _deleteAssets(delta.deletes); await _upsertAssets(delta.updates.map((a) => a.toLocalAsset())); - await _db.batch((batch) { - batch.deleteWhere( - _db.localAlbumAssetEntity, - (f) => f.assetId.isIn(delta.updates.map((a) => a.id)), - ); + await _db.batch((batch) async { for (final asset in delta.updates) { + batch.deleteWhere( + _db.localAlbumAssetEntity, + (f) => + f.albumId.isNotIn(asset.albumIds) & f.assetId.equals(asset.id), + ); + batch.insertAll( _db.localAlbumAssetEntity, asset.albumIds.map( @@ -192,6 +248,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository albumId: albumId, ), ), + onConflict: DoNothing(), ); } }); @@ -206,17 +263,14 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository return Future.value(); } - return _db.batch( - (batch) => batch.insertAll( - _db.localAlbumAssetEntity, - assets.map( - (a) => LocalAlbumAssetEntityCompanion.insert( - assetId: a.id, - albumId: albumId, - ), + return _db.localAlbumAssetEntity.insertAll( + assets.map( + (a) => LocalAlbumAssetEntityCompanion.insert( + assetId: a.id, + albumId: albumId, ), - mode: InsertMode.insertOrIgnore, ), + mode: InsertMode.insertOrIgnore, ); } @@ -272,7 +326,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository createdAt: Value(a.createdAt), updatedAt: Value(a.updatedAt), durationInSeconds: Value.absentIfNull(a.durationInSeconds), - localId: a.id, + id: a.id, checksum: Value.absentIfNull(a.checksum), ), ), @@ -288,7 +342,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository return _db.batch( (batch) => batch.deleteWhere( _db.localAssetEntity, - (f) => f.localId.isIn(ids), + (f) => f.id.isIn(ids), ), ); } diff --git a/mobile/lib/infrastructure/repositories/local_asset.repository.dart b/mobile/lib/infrastructure/repositories/local_asset.repository.dart index 2f28176d8d..92ab9efd60 100644 --- a/mobile/lib/infrastructure/repositories/local_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_asset.repository.dart @@ -10,7 +10,7 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository @override Future get(String id) => _db.managers.localAssetEntity - .filter((f) => f.localId(id)) + .filter((f) => f.id.equals(id)) .map((a) => a.toDto()) .getSingle(); } diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index 03a3cdff4a..0a1a85193b 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -180,7 +180,7 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository batch.insert( _db.remoteAssetEntity, - companion.copyWith(remoteId: Value(asset.id.toUuidByte())), + companion.copyWith(id: Value(asset.id.toUuidByte())), onConflict: DoUpdate((_) => companion), ); } @@ -191,9 +191,7 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository for (final asset in assets) { batch.delete( _db.remoteAssetEntity, - RemoteAssetEntityCompanion( - remoteId: Value(asset.assetId.toUuidByte()), - ), + RemoteAssetEntityCompanion(id: Value(asset.assetId.toUuidByte())), ); } }); diff --git a/mobile/lib/platform/messages.dart b/mobile/lib/platform/messages.dart index 84fdb04269..807543c9a9 100644 --- a/mobile/lib/platform/messages.dart +++ b/mobile/lib/platform/messages.dart @@ -7,7 +7,7 @@ import 'package:pigeon/pigeon.dart'; swiftOut: 'ios/Runner/Platform/Messages.g.swift', swiftOptions: SwiftOptions(), kotlinOut: - 'android/app/src/main/kotlin/app/alextran/immich/platform/messages.g.kt', + 'android/app/src/main/kotlin/app/alextran/immich/platform/Messages.g.kt', kotlinOptions: KotlinOptions(), dartOptions: DartOptions(), ), @@ -52,4 +52,8 @@ abstract class ImHostService { @async void checkpointSync(); + + @async + @TaskQueue(type: TaskQueueType.serialBackgroundThread) + List getAssetIdsForAlbum(String albumId); } diff --git a/mobile/lib/platform/messages.g.dart b/mobile/lib/platform/messages.g.dart index f4751ebf20..05a8dc24a5 100644 --- a/mobile/lib/platform/messages.g.dart +++ b/mobile/lib/platform/messages.g.dart @@ -14,22 +14,21 @@ PlatformException _createConnectionError(String channelName) { message: 'Unable to establish connection on channel: "$channelName".', ); } - bool _deepEquals(Object? a, Object? b) { if (a is List && b is List) { return a.length == b.length && a.indexed - .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); } if (a is Map && b is Map) { - return a.length == b.length && - a.entries.every((MapEntry entry) => - (b as Map).containsKey(entry.key) && - _deepEquals(entry.value, b[entry.key])); + return a.length == b.length && a.entries.every((MapEntry entry) => + (b as Map).containsKey(entry.key) && + _deepEquals(entry.value, b[entry.key])); } return a == b; } + class Asset { Asset({ required this.id, @@ -68,8 +67,7 @@ class Asset { } Object encode() { - return _toList(); - } + return _toList(); } static Asset decode(Object result) { result as List; @@ -98,7 +96,8 @@ class Asset { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } class SyncDelta { @@ -119,8 +118,7 @@ class SyncDelta { } Object encode() { - return _toList(); - } + return _toList(); } static SyncDelta decode(Object result) { result as List; @@ -144,9 +142,11 @@ class SyncDelta { @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => Object.hashAll(_toList()) +; } + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -154,10 +154,10 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is Asset) { + } else if (value is Asset) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is SyncDelta) { + } else if (value is SyncDelta) { buffer.putUint8(130); writeValue(buffer, value.encode()); } else { @@ -168,9 +168,9 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 129: + case 129: return Asset.decode(readValue(buffer)!); - case 130: + case 130: return SyncDelta.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -182,11 +182,9 @@ class ImHostService { /// Constructor for [ImHostService]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - ImHostService( - {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + ImHostService({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = - messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -194,10 +192,8 @@ class ImHostService { final String pigeonVar_messageChannelSuffix; Future shouldFullSync() async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.ImHostService.shouldFullSync$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( + final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.ImHostService.shouldFullSync$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, @@ -224,10 +220,8 @@ class ImHostService { } Future hasMediaChanges() async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.ImHostService.hasMediaChanges$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( + final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.ImHostService.hasMediaChanges$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, @@ -254,10 +248,8 @@ class ImHostService { } Future getMediaChanges() async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.ImHostService.getMediaChanges$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( + final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.ImHostService.getMediaChanges$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, @@ -284,10 +276,8 @@ class ImHostService { } Future checkpointSync() async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.ImHostService.checkpointSync$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( + final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.ImHostService.checkpointSync$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, @@ -307,4 +297,32 @@ class ImHostService { return; } } + + Future> getAssetIdsForAlbum(String albumId) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.ImHostService.getAssetIdsForAlbum$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([albumId]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + 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?)!.cast(); + } + } } diff --git a/mobile/makefile b/mobile/makefile index b0083b1495..c02540baf8 100644 --- a/mobile/makefile +++ b/mobile/makefile @@ -1,6 +1,7 @@ .PHONY: build watch create_app_icon create_splash build_release_android build: + dart run pigeon --input lib/platform/messages.dart dart run build_runner build --delete-conflicting-outputs watch: