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