mirror of https://github.com/immich-app/immich.git
feat: delta sync (#18428)
* feat: delta sync * fix: ignore iCloud assets * feat: dev logs * add full sync button * remove photo_manager dep for sync * misc logs and fix * add time taken to DLog * fix: build release iOS * ios sync go brrr * rename local sync service * update isar fork * rename to platform assets / albums * fix ci check --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex <alex.tran1502@gmail.com>flutter-3.32
parent
2b1b20ab0b
commit
dbdb64f6c5
@ -0,0 +1,393 @@
|
|||||||
|
// Autogenerated from Pigeon (v25.3.2), do not edit directly.
|
||||||
|
// See also: https://pub.dev/packages/pigeon
|
||||||
|
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||||
|
|
||||||
|
package app.alextran.immich.sync
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import io.flutter.plugin.common.BasicMessageChannel
|
||||||
|
import io.flutter.plugin.common.BinaryMessenger
|
||||||
|
import io.flutter.plugin.common.EventChannel
|
||||||
|
import io.flutter.plugin.common.MessageCodec
|
||||||
|
import io.flutter.plugin.common.StandardMethodCodec
|
||||||
|
import io.flutter.plugin.common.StandardMessageCodec
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
private object MessagesPigeonUtils {
|
||||||
|
|
||||||
|
fun wrapResult(result: Any?): List<Any?> {
|
||||||
|
return listOf(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun wrapError(exception: Throwable): List<Any?> {
|
||||||
|
return if (exception is FlutterError) {
|
||||||
|
listOf(
|
||||||
|
exception.code,
|
||||||
|
exception.message,
|
||||||
|
exception.details
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
listOf(
|
||||||
|
exception.javaClass.simpleName,
|
||||||
|
exception.toString(),
|
||||||
|
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun deepEquals(a: Any?, b: Any?): Boolean {
|
||||||
|
if (a is ByteArray && b is ByteArray) {
|
||||||
|
return a.contentEquals(b)
|
||||||
|
}
|
||||||
|
if (a is IntArray && b is IntArray) {
|
||||||
|
return a.contentEquals(b)
|
||||||
|
}
|
||||||
|
if (a is LongArray && b is LongArray) {
|
||||||
|
return a.contentEquals(b)
|
||||||
|
}
|
||||||
|
if (a is DoubleArray && b is DoubleArray) {
|
||||||
|
return a.contentEquals(b)
|
||||||
|
}
|
||||||
|
if (a is Array<*> && b is Array<*>) {
|
||||||
|
return a.size == b.size &&
|
||||||
|
a.indices.all{ deepEquals(a[it], b[it]) }
|
||||||
|
}
|
||||||
|
if (a is List<*> && b is List<*>) {
|
||||||
|
return a.size == b.size &&
|
||||||
|
a.indices.all{ deepEquals(a[it], b[it]) }
|
||||||
|
}
|
||||||
|
if (a is Map<*, *> && b is Map<*, *>) {
|
||||||
|
return a.size == b.size && a.all {
|
||||||
|
(b as Map<Any?, Any?>).containsKey(it.key) &&
|
||||||
|
deepEquals(it.value, b[it.key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a == b
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error class for passing custom error details to Flutter via a thrown PlatformException.
|
||||||
|
* @property code The error code.
|
||||||
|
* @property message The error message.
|
||||||
|
* @property details The error details. Must be a datatype supported by the api codec.
|
||||||
|
*/
|
||||||
|
class FlutterError (
|
||||||
|
val code: String,
|
||||||
|
override val message: String? = null,
|
||||||
|
val details: Any? = null
|
||||||
|
) : Throwable()
|
||||||
|
|
||||||
|
/** Generated class from Pigeon that represents data sent in messages. */
|
||||||
|
data class PlatformAsset (
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val type: Long,
|
||||||
|
val createdAt: Long? = null,
|
||||||
|
val updatedAt: Long? = null,
|
||||||
|
val durationInSeconds: Long
|
||||||
|
)
|
||||||
|
{
|
||||||
|
companion object {
|
||||||
|
fun fromList(pigeonVar_list: List<Any?>): PlatformAsset {
|
||||||
|
val id = pigeonVar_list[0] as String
|
||||||
|
val name = pigeonVar_list[1] as String
|
||||||
|
val type = pigeonVar_list[2] as Long
|
||||||
|
val createdAt = pigeonVar_list[3] as Long?
|
||||||
|
val updatedAt = pigeonVar_list[4] as Long?
|
||||||
|
val durationInSeconds = pigeonVar_list[5] as Long
|
||||||
|
return PlatformAsset(id, name, type, createdAt, updatedAt, durationInSeconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun toList(): List<Any?> {
|
||||||
|
return listOf(
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
durationInSeconds,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other !is PlatformAsset) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (this === other) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return MessagesPigeonUtils.deepEquals(toList(), other.toList()) }
|
||||||
|
|
||||||
|
override fun hashCode(): Int = toList().hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generated class from Pigeon that represents data sent in messages. */
|
||||||
|
data class PlatformAlbum (
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val updatedAt: Long? = null,
|
||||||
|
val isCloud: Boolean,
|
||||||
|
val assetCount: Long
|
||||||
|
)
|
||||||
|
{
|
||||||
|
companion object {
|
||||||
|
fun fromList(pigeonVar_list: List<Any?>): PlatformAlbum {
|
||||||
|
val id = pigeonVar_list[0] as String
|
||||||
|
val name = pigeonVar_list[1] as String
|
||||||
|
val updatedAt = pigeonVar_list[2] as Long?
|
||||||
|
val isCloud = pigeonVar_list[3] as Boolean
|
||||||
|
val assetCount = pigeonVar_list[4] as Long
|
||||||
|
return PlatformAlbum(id, name, updatedAt, isCloud, assetCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun toList(): List<Any?> {
|
||||||
|
return listOf(
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
updatedAt,
|
||||||
|
isCloud,
|
||||||
|
assetCount,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other !is PlatformAlbum) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (this === other) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return MessagesPigeonUtils.deepEquals(toList(), other.toList()) }
|
||||||
|
|
||||||
|
override fun hashCode(): Int = toList().hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generated class from Pigeon that represents data sent in messages. */
|
||||||
|
data class SyncDelta (
|
||||||
|
val hasChanges: Boolean,
|
||||||
|
val updates: List<PlatformAsset>,
|
||||||
|
val deletes: List<String>,
|
||||||
|
val assetAlbums: Map<String, List<String>>
|
||||||
|
)
|
||||||
|
{
|
||||||
|
companion object {
|
||||||
|
fun fromList(pigeonVar_list: List<Any?>): SyncDelta {
|
||||||
|
val hasChanges = pigeonVar_list[0] as Boolean
|
||||||
|
val updates = pigeonVar_list[1] as List<PlatformAsset>
|
||||||
|
val deletes = pigeonVar_list[2] as List<String>
|
||||||
|
val assetAlbums = pigeonVar_list[3] as Map<String, List<String>>
|
||||||
|
return SyncDelta(hasChanges, updates, deletes, assetAlbums)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun toList(): List<Any?> {
|
||||||
|
return listOf(
|
||||||
|
hasChanges,
|
||||||
|
updates,
|
||||||
|
deletes,
|
||||||
|
assetAlbums,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other !is SyncDelta) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (this === other) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return MessagesPigeonUtils.deepEquals(toList(), other.toList()) }
|
||||||
|
|
||||||
|
override fun hashCode(): Int = toList().hashCode()
|
||||||
|
}
|
||||||
|
private open class MessagesPigeonCodec : StandardMessageCodec() {
|
||||||
|
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||||
|
return when (type) {
|
||||||
|
129.toByte() -> {
|
||||||
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
|
PlatformAsset.fromList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
130.toByte() -> {
|
||||||
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
|
PlatformAlbum.fromList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
131.toByte() -> {
|
||||||
|
return (readValue(buffer) as? List<Any?>)?.let {
|
||||||
|
SyncDelta.fromList(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> super.readValueOfType(type, buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||||
|
when (value) {
|
||||||
|
is PlatformAsset -> {
|
||||||
|
stream.write(129)
|
||||||
|
writeValue(stream, value.toList())
|
||||||
|
}
|
||||||
|
is PlatformAlbum -> {
|
||||||
|
stream.write(130)
|
||||||
|
writeValue(stream, value.toList())
|
||||||
|
}
|
||||||
|
is SyncDelta -> {
|
||||||
|
stream.write(131)
|
||||||
|
writeValue(stream, value.toList())
|
||||||
|
}
|
||||||
|
else -> super.writeValue(stream, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
|
interface NativeSyncApi {
|
||||||
|
fun shouldFullSync(): Boolean
|
||||||
|
fun getMediaChanges(): SyncDelta
|
||||||
|
fun checkpointSync()
|
||||||
|
fun clearSyncCheckpoint()
|
||||||
|
fun getAssetIdsForAlbum(albumId: String): List<String>
|
||||||
|
fun getAlbums(): List<PlatformAlbum>
|
||||||
|
fun getAssetsCountSince(albumId: String, timestamp: Long): Long
|
||||||
|
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<PlatformAsset>
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** The codec used by NativeSyncApi. */
|
||||||
|
val codec: MessageCodec<Any?> by lazy {
|
||||||
|
MessagesPigeonCodec()
|
||||||
|
}
|
||||||
|
/** Sets up an instance of `NativeSyncApi` to handle messages through the `binaryMessenger`. */
|
||||||
|
@JvmOverloads
|
||||||
|
fun setUp(binaryMessenger: BinaryMessenger, api: NativeSyncApi?, messageChannelSuffix: String = "") {
|
||||||
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
|
val taskQueue = binaryMessenger.makeBackgroundTaskQueue()
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
val wrapped: List<Any?> = try {
|
||||||
|
listOf(api.shouldFullSync())
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
MessagesPigeonUtils.wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$separatedMessageChannelSuffix", codec, taskQueue)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
val wrapped: List<Any?> = try {
|
||||||
|
listOf(api.getMediaChanges())
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
MessagesPigeonUtils.wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
val wrapped: List<Any?> = try {
|
||||||
|
api.checkpointSync()
|
||||||
|
listOf(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
MessagesPigeonUtils.wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
val wrapped: List<Any?> = try {
|
||||||
|
api.clearSyncCheckpoint()
|
||||||
|
listOf(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
MessagesPigeonUtils.wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum$separatedMessageChannelSuffix", codec, taskQueue)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val albumIdArg = args[0] as String
|
||||||
|
val wrapped: List<Any?> = try {
|
||||||
|
listOf(api.getAssetIdsForAlbum(albumIdArg))
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
MessagesPigeonUtils.wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$separatedMessageChannelSuffix", codec, taskQueue)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
val wrapped: List<Any?> = try {
|
||||||
|
listOf(api.getAlbums())
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
MessagesPigeonUtils.wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince$separatedMessageChannelSuffix", codec, taskQueue)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val albumIdArg = args[0] as String
|
||||||
|
val timestampArg = args[1] as Long
|
||||||
|
val wrapped: List<Any?> = try {
|
||||||
|
listOf(api.getAssetsCountSince(albumIdArg, timestampArg))
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
MessagesPigeonUtils.wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$separatedMessageChannelSuffix", codec, taskQueue)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val albumIdArg = args[0] as String
|
||||||
|
val updatedTimeCondArg = args[1] as Long?
|
||||||
|
val wrapped: List<Any?> = try {
|
||||||
|
listOf(api.getAssetsForAlbum(albumIdArg, updatedTimeCondArg))
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
MessagesPigeonUtils.wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package app.alextran.immich.sync
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
|
||||||
|
class NativeSyncApiImpl26(context: Context) : NativeSyncApiImplBase(context), NativeSyncApi {
|
||||||
|
override fun shouldFullSync(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// No-op for Android 10 and below
|
||||||
|
override fun checkpointSync() {
|
||||||
|
// Cannot throw exception as this is called from the Dart side
|
||||||
|
// during the full sync process as well
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearSyncCheckpoint() {
|
||||||
|
// No-op for Android 10 and below
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMediaChanges(): SyncDelta {
|
||||||
|
throw IllegalStateException("Method not supported on this Android version.")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
package app.alextran.immich.sync
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.annotation.RequiresExtension
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
|
@RequiresExtension(extension = Build.VERSION_CODES.R, version = 1)
|
||||||
|
class NativeSyncApiImpl30(context: Context) : NativeSyncApiImplBase(context), NativeSyncApi {
|
||||||
|
private val ctx: Context = context.applicationContext
|
||||||
|
private val prefs = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val SHARED_PREF_NAME = "Immich::MediaManager"
|
||||||
|
const val SHARED_PREF_MEDIA_STORE_VERSION_KEY = "MediaStore::getVersion"
|
||||||
|
const val SHARED_PREF_MEDIA_STORE_GEN_KEY = "MediaStore::getGeneration"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSavedGenerationMap(): Map<String, Long> {
|
||||||
|
return prefs.getString(SHARED_PREF_MEDIA_STORE_GEN_KEY, null)?.let {
|
||||||
|
Json.decodeFromString<Map<String, Long>>(it)
|
||||||
|
} ?: emptyMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearSyncCheckpoint() {
|
||||||
|
prefs.edit().apply {
|
||||||
|
remove(SHARED_PREF_MEDIA_STORE_VERSION_KEY)
|
||||||
|
remove(SHARED_PREF_MEDIA_STORE_GEN_KEY)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shouldFullSync(): Boolean =
|
||||||
|
MediaStore.getVersion(ctx) != prefs.getString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, null)
|
||||||
|
|
||||||
|
override fun checkpointSync() {
|
||||||
|
val genMap = MediaStore.getExternalVolumeNames(ctx)
|
||||||
|
.associateWith { MediaStore.getGeneration(ctx, it) }
|
||||||
|
|
||||||
|
prefs.edit().apply {
|
||||||
|
putString(SHARED_PREF_MEDIA_STORE_VERSION_KEY, MediaStore.getVersion(ctx))
|
||||||
|
putString(SHARED_PREF_MEDIA_STORE_GEN_KEY, Json.encodeToString(genMap))
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMediaChanges(): SyncDelta {
|
||||||
|
val genMap = getSavedGenerationMap()
|
||||||
|
val currentVolumes = MediaStore.getExternalVolumeNames(ctx)
|
||||||
|
val changed = mutableListOf<PlatformAsset>()
|
||||||
|
val deleted = mutableListOf<String>()
|
||||||
|
val assetAlbums = mutableMapOf<String, List<String>>()
|
||||||
|
var hasChanges = genMap.keys != currentVolumes
|
||||||
|
|
||||||
|
for (volume in currentVolumes) {
|
||||||
|
val currentGen = MediaStore.getGeneration(ctx, volume)
|
||||||
|
val storedGen = genMap[volume] ?: 0
|
||||||
|
if (currentGen <= storedGen) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
hasChanges = true
|
||||||
|
|
||||||
|
val selection =
|
||||||
|
"$MEDIA_SELECTION AND (${MediaStore.MediaColumns.GENERATION_MODIFIED} > ? OR ${MediaStore.MediaColumns.GENERATION_ADDED} > ?)"
|
||||||
|
val selectionArgs = arrayOf(
|
||||||
|
*MEDIA_SELECTION_ARGS,
|
||||||
|
storedGen.toString(),
|
||||||
|
storedGen.toString()
|
||||||
|
)
|
||||||
|
|
||||||
|
getAssets(getCursor(volume, selection, selectionArgs)).forEach {
|
||||||
|
when (it) {
|
||||||
|
is AssetResult.ValidAsset -> {
|
||||||
|
changed.add(it.asset)
|
||||||
|
assetAlbums[it.asset.id] = listOf(it.albumId)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AssetResult.InvalidAsset -> deleted.add(it.assetId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Unmounted volumes are handled in dart when the album is removed
|
||||||
|
return SyncDelta(hasChanges, changed, deleted, assetAlbums)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,177 @@
|
|||||||
|
package app.alextran.immich.sync
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
sealed class AssetResult {
|
||||||
|
data class ValidAsset(val asset: PlatformAsset, val albumId: String) : AssetResult()
|
||||||
|
data class InvalidAsset(val assetId: String) : AssetResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("InlinedApi")
|
||||||
|
open class NativeSyncApiImplBase(context: Context) {
|
||||||
|
private val ctx: Context = context.applicationContext
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val MEDIA_SELECTION =
|
||||||
|
"(${MediaStore.Files.FileColumns.MEDIA_TYPE} = ? OR ${MediaStore.Files.FileColumns.MEDIA_TYPE} = ?)"
|
||||||
|
val MEDIA_SELECTION_ARGS = arrayOf(
|
||||||
|
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE.toString(),
|
||||||
|
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString()
|
||||||
|
)
|
||||||
|
const val BUCKET_SELECTION = "(${MediaStore.Files.FileColumns.BUCKET_ID} = ?)"
|
||||||
|
val ASSET_PROJECTION = arrayOf(
|
||||||
|
MediaStore.MediaColumns._ID,
|
||||||
|
MediaStore.MediaColumns.DATA,
|
||||||
|
MediaStore.MediaColumns.DISPLAY_NAME,
|
||||||
|
MediaStore.MediaColumns.DATE_TAKEN,
|
||||||
|
MediaStore.MediaColumns.DATE_ADDED,
|
||||||
|
MediaStore.MediaColumns.DATE_MODIFIED,
|
||||||
|
MediaStore.Files.FileColumns.MEDIA_TYPE,
|
||||||
|
MediaStore.MediaColumns.BUCKET_ID,
|
||||||
|
MediaStore.MediaColumns.DURATION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun getCursor(
|
||||||
|
volume: String,
|
||||||
|
selection: String,
|
||||||
|
selectionArgs: Array<String>,
|
||||||
|
projection: Array<String> = ASSET_PROJECTION,
|
||||||
|
sortOrder: String? = null
|
||||||
|
): Cursor? = ctx.contentResolver.query(
|
||||||
|
MediaStore.Files.getContentUri(volume),
|
||||||
|
projection,
|
||||||
|
selection,
|
||||||
|
selectionArgs,
|
||||||
|
sortOrder,
|
||||||
|
)
|
||||||
|
|
||||||
|
protected fun getAssets(cursor: Cursor?): Sequence<AssetResult> {
|
||||||
|
return sequence {
|
||||||
|
cursor?.use { c ->
|
||||||
|
val idColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
|
||||||
|
val dataColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
|
||||||
|
val nameColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)
|
||||||
|
val dateTakenColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_TAKEN)
|
||||||
|
val dateAddedColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED)
|
||||||
|
val dateModifiedColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)
|
||||||
|
val mediaTypeColumn = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE)
|
||||||
|
val bucketIdColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.BUCKET_ID)
|
||||||
|
val durationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION)
|
||||||
|
|
||||||
|
while (c.moveToNext()) {
|
||||||
|
val id = c.getLong(idColumn).toString()
|
||||||
|
|
||||||
|
val path = c.getString(dataColumn)
|
||||||
|
if (path.isNullOrBlank() || !File(path).exists()) {
|
||||||
|
yield(AssetResult.InvalidAsset(id))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val mediaType = c.getInt(mediaTypeColumn)
|
||||||
|
val name = c.getString(nameColumn)
|
||||||
|
// Date taken is milliseconds since epoch, Date added is seconds since epoch
|
||||||
|
val createdAt = (c.getLong(dateTakenColumn).takeIf { it > 0 }?.div(1000))
|
||||||
|
?: c.getLong(dateAddedColumn)
|
||||||
|
// Date modified is seconds since epoch
|
||||||
|
val modifiedAt = c.getLong(dateModifiedColumn)
|
||||||
|
// Duration is milliseconds
|
||||||
|
val duration = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0
|
||||||
|
else c.getLong(durationColumn) / 1000
|
||||||
|
val bucketId = c.getString(bucketIdColumn)
|
||||||
|
|
||||||
|
val asset = PlatformAsset(id, name, mediaType.toLong(), createdAt, modifiedAt, duration)
|
||||||
|
yield(AssetResult.ValidAsset(asset, bucketId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAlbums(): List<PlatformAlbum> {
|
||||||
|
val albums = mutableListOf<PlatformAlbum>()
|
||||||
|
val albumsCount = mutableMapOf<String, Int>()
|
||||||
|
|
||||||
|
val projection = arrayOf(
|
||||||
|
MediaStore.Files.FileColumns.BUCKET_ID,
|
||||||
|
MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME,
|
||||||
|
MediaStore.Files.FileColumns.DATE_MODIFIED,
|
||||||
|
)
|
||||||
|
val selection =
|
||||||
|
"(${MediaStore.Files.FileColumns.BUCKET_ID} IS NOT NULL) AND $MEDIA_SELECTION"
|
||||||
|
|
||||||
|
getCursor(
|
||||||
|
MediaStore.VOLUME_EXTERNAL,
|
||||||
|
selection,
|
||||||
|
MEDIA_SELECTION_ARGS,
|
||||||
|
projection,
|
||||||
|
"${MediaStore.Files.FileColumns.DATE_MODIFIED} DESC"
|
||||||
|
)?.use { cursor ->
|
||||||
|
val bucketIdColumn =
|
||||||
|
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.BUCKET_ID)
|
||||||
|
val bucketNameColumn =
|
||||||
|
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.BUCKET_DISPLAY_NAME)
|
||||||
|
val dateModified =
|
||||||
|
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_MODIFIED)
|
||||||
|
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
val id = cursor.getString(bucketIdColumn)
|
||||||
|
|
||||||
|
val count = albumsCount.getOrDefault(id, 0)
|
||||||
|
if (count != 0) {
|
||||||
|
albumsCount[id] = count + 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val name = cursor.getString(bucketNameColumn)
|
||||||
|
val updatedAt = cursor.getLong(dateModified)
|
||||||
|
albums.add(PlatformAlbum(id, name, updatedAt, false, 0))
|
||||||
|
albumsCount[id] = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return albums.map { it.copy(assetCount = albumsCount[it.id]?.toLong() ?: 0) }
|
||||||
|
.sortedBy { it.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAssetIdsForAlbum(albumId: String): List<String> {
|
||||||
|
val projection = arrayOf(MediaStore.MediaColumns._ID)
|
||||||
|
|
||||||
|
return getCursor(
|
||||||
|
MediaStore.VOLUME_EXTERNAL,
|
||||||
|
"$BUCKET_SELECTION AND $MEDIA_SELECTION",
|
||||||
|
arrayOf(albumId, *MEDIA_SELECTION_ARGS),
|
||||||
|
projection
|
||||||
|
)?.use { cursor ->
|
||||||
|
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
|
||||||
|
generateSequence {
|
||||||
|
if (cursor.moveToNext()) cursor.getLong(idColumn).toString() else null
|
||||||
|
}.toList()
|
||||||
|
} ?: emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAssetsCountSince(albumId: String, timestamp: Long): Long =
|
||||||
|
getCursor(
|
||||||
|
MediaStore.VOLUME_EXTERNAL,
|
||||||
|
"$BUCKET_SELECTION AND ${MediaStore.Files.FileColumns.DATE_ADDED} > ? AND $MEDIA_SELECTION",
|
||||||
|
arrayOf(albumId, timestamp.toString(), *MEDIA_SELECTION_ARGS),
|
||||||
|
)?.use { cursor -> cursor.count.toLong() } ?: 0L
|
||||||
|
|
||||||
|
|
||||||
|
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<PlatformAsset> {
|
||||||
|
var selection = "$BUCKET_SELECTION AND $MEDIA_SELECTION"
|
||||||
|
val selectionArgs = mutableListOf(albumId, *MEDIA_SELECTION_ARGS)
|
||||||
|
|
||||||
|
if (updatedTimeCond != null) {
|
||||||
|
selection += " AND (${MediaStore.Files.FileColumns.DATE_MODIFIED} > ? OR ${MediaStore.Files.FileColumns.DATE_ADDED} > ?)"
|
||||||
|
selectionArgs.addAll(listOf(updatedTimeCond.toString(), updatedTimeCond.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return getAssets(getCursor(MediaStore.VOLUME_EXTERNAL, selection, selectionArgs.toTypedArray()))
|
||||||
|
.mapNotNull { result -> (result as? AssetResult.ValidAsset)?.asset }
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,26 +1,27 @@
|
|||||||
pluginManagement {
|
pluginManagement {
|
||||||
def flutterSdkPath = {
|
def flutterSdkPath = {
|
||||||
def properties = new Properties()
|
def properties = new Properties()
|
||||||
file("local.properties").withInputStream { properties.load(it) }
|
file("local.properties").withInputStream { properties.load(it) }
|
||||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||||
return flutterSdkPath
|
return flutterSdkPath
|
||||||
}()
|
}()
|
||||||
|
|
||||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version '8.7.2' apply false
|
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.android" version "2.0.20" apply false
|
||||||
id 'com.google.devtools.ksp' version '2.0.20-1.0.24' 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"
|
include ":app"
|
||||||
|
|||||||
@ -0,0 +1,446 @@
|
|||||||
|
// Autogenerated from Pigeon (v25.3.2), do not edit directly.
|
||||||
|
// See also: https://pub.dev/packages/pigeon
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
import Flutter
|
||||||
|
#elseif os(macOS)
|
||||||
|
import FlutterMacOS
|
||||||
|
#else
|
||||||
|
#error("Unsupported platform.")
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// Error class for passing custom error details to Dart side.
|
||||||
|
final class PigeonError: Error {
|
||||||
|
let code: String
|
||||||
|
let message: String?
|
||||||
|
let details: Sendable?
|
||||||
|
|
||||||
|
init(code: String, message: String?, details: Sendable?) {
|
||||||
|
self.code = code
|
||||||
|
self.message = message
|
||||||
|
self.details = details
|
||||||
|
}
|
||||||
|
|
||||||
|
var localizedDescription: String {
|
||||||
|
return
|
||||||
|
"PigeonError(code: \(code), message: \(message ?? "<nil>"), details: \(details ?? "<nil>")"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func wrapResult(_ result: Any?) -> [Any?] {
|
||||||
|
return [result]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func wrapError(_ error: Any) -> [Any?] {
|
||||||
|
if let pigeonError = error as? PigeonError {
|
||||||
|
return [
|
||||||
|
pigeonError.code,
|
||||||
|
pigeonError.message,
|
||||||
|
pigeonError.details,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if let flutterError = error as? FlutterError {
|
||||||
|
return [
|
||||||
|
flutterError.code,
|
||||||
|
flutterError.message,
|
||||||
|
flutterError.details,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
"\(error)",
|
||||||
|
"\(type(of: error))",
|
||||||
|
"Stacktrace: \(Thread.callStackSymbols)",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isNullish(_ value: Any?) -> Bool {
|
||||||
|
return value is NSNull || value == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func nilOrValue<T>(_ value: Any?) -> T? {
|
||||||
|
if value is NSNull { return nil }
|
||||||
|
return value as! T?
|
||||||
|
}
|
||||||
|
|
||||||
|
func deepEqualsMessages(_ lhs: Any?, _ rhs: Any?) -> Bool {
|
||||||
|
let cleanLhs = nilOrValue(lhs) as Any?
|
||||||
|
let cleanRhs = nilOrValue(rhs) as Any?
|
||||||
|
switch (cleanLhs, cleanRhs) {
|
||||||
|
case (nil, nil):
|
||||||
|
return true
|
||||||
|
|
||||||
|
case (nil, _), (_, nil):
|
||||||
|
return false
|
||||||
|
|
||||||
|
case is (Void, Void):
|
||||||
|
return true
|
||||||
|
|
||||||
|
case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable):
|
||||||
|
return cleanLhsHashable == cleanRhsHashable
|
||||||
|
|
||||||
|
case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]):
|
||||||
|
guard cleanLhsArray.count == cleanRhsArray.count else { return false }
|
||||||
|
for (index, element) in cleanLhsArray.enumerated() {
|
||||||
|
if !deepEqualsMessages(element, cleanRhsArray[index]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
|
||||||
|
case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
|
||||||
|
guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false }
|
||||||
|
for (key, cleanLhsValue) in cleanLhsDictionary {
|
||||||
|
guard cleanRhsDictionary.index(forKey: key) != nil else { return false }
|
||||||
|
if !deepEqualsMessages(cleanLhsValue, cleanRhsDictionary[key]!) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deepHashMessages(value: Any?, hasher: inout Hasher) {
|
||||||
|
if let valueList = value as? [AnyHashable] {
|
||||||
|
for item in valueList { deepHashMessages(value: item, hasher: &hasher) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let valueDict = value as? [AnyHashable: AnyHashable] {
|
||||||
|
for key in valueDict.keys {
|
||||||
|
hasher.combine(key)
|
||||||
|
deepHashMessages(value: valueDict[key]!, hasher: &hasher)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let hashableValue = value as? AnyHashable {
|
||||||
|
hasher.combine(hashableValue.hashValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasher.combine(String(describing: value))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Generated class from Pigeon that represents data sent in messages.
|
||||||
|
struct PlatformAsset: Hashable {
|
||||||
|
var id: String
|
||||||
|
var name: String
|
||||||
|
var type: Int64
|
||||||
|
var createdAt: Int64? = nil
|
||||||
|
var updatedAt: Int64? = nil
|
||||||
|
var durationInSeconds: Int64
|
||||||
|
|
||||||
|
|
||||||
|
// swift-format-ignore: AlwaysUseLowerCamelCase
|
||||||
|
static func fromList(_ pigeonVar_list: [Any?]) -> PlatformAsset? {
|
||||||
|
let id = pigeonVar_list[0] as! String
|
||||||
|
let name = pigeonVar_list[1] as! String
|
||||||
|
let type = pigeonVar_list[2] as! Int64
|
||||||
|
let createdAt: Int64? = nilOrValue(pigeonVar_list[3])
|
||||||
|
let updatedAt: Int64? = nilOrValue(pigeonVar_list[4])
|
||||||
|
let durationInSeconds = pigeonVar_list[5] as! Int64
|
||||||
|
|
||||||
|
return PlatformAsset(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
type: type,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
durationInSeconds: durationInSeconds
|
||||||
|
)
|
||||||
|
}
|
||||||
|
func toList() -> [Any?] {
|
||||||
|
return [
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
durationInSeconds,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
|
||||||
|
return deepEqualsMessages(lhs.toList(), rhs.toList()) }
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
deepHashMessages(value: toList(), hasher: &hasher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generated class from Pigeon that represents data sent in messages.
|
||||||
|
struct PlatformAlbum: Hashable {
|
||||||
|
var id: String
|
||||||
|
var name: String
|
||||||
|
var updatedAt: Int64? = nil
|
||||||
|
var isCloud: Bool
|
||||||
|
var assetCount: Int64
|
||||||
|
|
||||||
|
|
||||||
|
// swift-format-ignore: AlwaysUseLowerCamelCase
|
||||||
|
static func fromList(_ pigeonVar_list: [Any?]) -> PlatformAlbum? {
|
||||||
|
let id = pigeonVar_list[0] as! String
|
||||||
|
let name = pigeonVar_list[1] as! String
|
||||||
|
let updatedAt: Int64? = nilOrValue(pigeonVar_list[2])
|
||||||
|
let isCloud = pigeonVar_list[3] as! Bool
|
||||||
|
let assetCount = pigeonVar_list[4] as! Int64
|
||||||
|
|
||||||
|
return PlatformAlbum(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
isCloud: isCloud,
|
||||||
|
assetCount: assetCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
func toList() -> [Any?] {
|
||||||
|
return [
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
updatedAt,
|
||||||
|
isCloud,
|
||||||
|
assetCount,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
static func == (lhs: PlatformAlbum, rhs: PlatformAlbum) -> Bool {
|
||||||
|
return deepEqualsMessages(lhs.toList(), rhs.toList()) }
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
deepHashMessages(value: toList(), hasher: &hasher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generated class from Pigeon that represents data sent in messages.
|
||||||
|
struct SyncDelta: Hashable {
|
||||||
|
var hasChanges: Bool
|
||||||
|
var updates: [PlatformAsset]
|
||||||
|
var deletes: [String]
|
||||||
|
var assetAlbums: [String: [String]]
|
||||||
|
|
||||||
|
|
||||||
|
// swift-format-ignore: AlwaysUseLowerCamelCase
|
||||||
|
static func fromList(_ pigeonVar_list: [Any?]) -> SyncDelta? {
|
||||||
|
let hasChanges = pigeonVar_list[0] as! Bool
|
||||||
|
let updates = pigeonVar_list[1] as! [PlatformAsset]
|
||||||
|
let deletes = pigeonVar_list[2] as! [String]
|
||||||
|
let assetAlbums = pigeonVar_list[3] as! [String: [String]]
|
||||||
|
|
||||||
|
return SyncDelta(
|
||||||
|
hasChanges: hasChanges,
|
||||||
|
updates: updates,
|
||||||
|
deletes: deletes,
|
||||||
|
assetAlbums: assetAlbums
|
||||||
|
)
|
||||||
|
}
|
||||||
|
func toList() -> [Any?] {
|
||||||
|
return [
|
||||||
|
hasChanges,
|
||||||
|
updates,
|
||||||
|
deletes,
|
||||||
|
assetAlbums,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
static func == (lhs: SyncDelta, rhs: SyncDelta) -> Bool {
|
||||||
|
return deepEqualsMessages(lhs.toList(), rhs.toList()) }
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
deepHashMessages(value: toList(), hasher: &hasher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MessagesPigeonCodecReader: FlutterStandardReader {
|
||||||
|
override func readValue(ofType type: UInt8) -> Any? {
|
||||||
|
switch type {
|
||||||
|
case 129:
|
||||||
|
return PlatformAsset.fromList(self.readValue() as! [Any?])
|
||||||
|
case 130:
|
||||||
|
return PlatformAlbum.fromList(self.readValue() as! [Any?])
|
||||||
|
case 131:
|
||||||
|
return SyncDelta.fromList(self.readValue() as! [Any?])
|
||||||
|
default:
|
||||||
|
return super.readValue(ofType: type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MessagesPigeonCodecWriter: FlutterStandardWriter {
|
||||||
|
override func writeValue(_ value: Any) {
|
||||||
|
if let value = value as? PlatformAsset {
|
||||||
|
super.writeByte(129)
|
||||||
|
super.writeValue(value.toList())
|
||||||
|
} else if let value = value as? PlatformAlbum {
|
||||||
|
super.writeByte(130)
|
||||||
|
super.writeValue(value.toList())
|
||||||
|
} else if let value = value as? SyncDelta {
|
||||||
|
super.writeByte(131)
|
||||||
|
super.writeValue(value.toList())
|
||||||
|
} else {
|
||||||
|
super.writeValue(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MessagesPigeonCodecReaderWriter: FlutterStandardReaderWriter {
|
||||||
|
override func reader(with data: Data) -> FlutterStandardReader {
|
||||||
|
return MessagesPigeonCodecReader(data: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func writer(with data: NSMutableData) -> FlutterStandardWriter {
|
||||||
|
return MessagesPigeonCodecWriter(data: data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
|
||||||
|
static let shared = MessagesPigeonCodec(readerWriter: MessagesPigeonCodecReaderWriter())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||||
|
protocol NativeSyncApi {
|
||||||
|
func shouldFullSync() throws -> Bool
|
||||||
|
func getMediaChanges() throws -> SyncDelta
|
||||||
|
func checkpointSync() throws
|
||||||
|
func clearSyncCheckpoint() throws
|
||||||
|
func getAssetIdsForAlbum(albumId: String) throws -> [String]
|
||||||
|
func getAlbums() throws -> [PlatformAlbum]
|
||||||
|
func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64
|
||||||
|
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||||
|
class NativeSyncApiSetup {
|
||||||
|
static var codec: FlutterStandardMessageCodec { MessagesPigeonCodec.shared }
|
||||||
|
/// Sets up an instance of `NativeSyncApi` to handle messages through the `binaryMessenger`.
|
||||||
|
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: NativeSyncApi?, messageChannelSuffix: String = "") {
|
||||||
|
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
||||||
|
#if os(iOS)
|
||||||
|
let taskQueue = binaryMessenger.makeBackgroundTaskQueue?()
|
||||||
|
#else
|
||||||
|
let taskQueue: FlutterTaskQueue? = nil
|
||||||
|
#endif
|
||||||
|
let shouldFullSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
shouldFullSyncChannel.setMessageHandler { _, reply in
|
||||||
|
do {
|
||||||
|
let result = try api.shouldFullSync()
|
||||||
|
reply(wrapResult(result))
|
||||||
|
} catch {
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shouldFullSyncChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let getMediaChangesChannel = taskQueue == nil
|
||||||
|
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
||||||
|
if let api = api {
|
||||||
|
getMediaChangesChannel.setMessageHandler { _, reply in
|
||||||
|
do {
|
||||||
|
let result = try api.getMediaChanges()
|
||||||
|
reply(wrapResult(result))
|
||||||
|
} catch {
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getMediaChangesChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let checkpointSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
checkpointSyncChannel.setMessageHandler { _, reply in
|
||||||
|
do {
|
||||||
|
try api.checkpointSync()
|
||||||
|
reply(wrapResult(nil))
|
||||||
|
} catch {
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
checkpointSyncChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let clearSyncCheckpointChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
clearSyncCheckpointChannel.setMessageHandler { _, reply in
|
||||||
|
do {
|
||||||
|
try api.clearSyncCheckpoint()
|
||||||
|
reply(wrapResult(nil))
|
||||||
|
} catch {
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clearSyncCheckpointChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let getAssetIdsForAlbumChannel = taskQueue == nil
|
||||||
|
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
||||||
|
if let api = api {
|
||||||
|
getAssetIdsForAlbumChannel.setMessageHandler { message, reply in
|
||||||
|
let args = message as! [Any?]
|
||||||
|
let albumIdArg = args[0] as! String
|
||||||
|
do {
|
||||||
|
let result = try api.getAssetIdsForAlbum(albumId: albumIdArg)
|
||||||
|
reply(wrapResult(result))
|
||||||
|
} catch {
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getAssetIdsForAlbumChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let getAlbumsChannel = taskQueue == nil
|
||||||
|
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
||||||
|
if let api = api {
|
||||||
|
getAlbumsChannel.setMessageHandler { _, reply in
|
||||||
|
do {
|
||||||
|
let result = try api.getAlbums()
|
||||||
|
reply(wrapResult(result))
|
||||||
|
} catch {
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getAlbumsChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let getAssetsCountSinceChannel = taskQueue == nil
|
||||||
|
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
||||||
|
if let api = api {
|
||||||
|
getAssetsCountSinceChannel.setMessageHandler { message, reply in
|
||||||
|
let args = message as! [Any?]
|
||||||
|
let albumIdArg = args[0] as! String
|
||||||
|
let timestampArg = args[1] as! Int64
|
||||||
|
do {
|
||||||
|
let result = try api.getAssetsCountSince(albumId: albumIdArg, timestamp: timestampArg)
|
||||||
|
reply(wrapResult(result))
|
||||||
|
} catch {
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getAssetsCountSinceChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let getAssetsForAlbumChannel = taskQueue == nil
|
||||||
|
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
||||||
|
if let api = api {
|
||||||
|
getAssetsForAlbumChannel.setMessageHandler { message, reply in
|
||||||
|
let args = message as! [Any?]
|
||||||
|
let albumIdArg = args[0] as! String
|
||||||
|
let updatedTimeCondArg: Int64? = nilOrValue(args[1])
|
||||||
|
do {
|
||||||
|
let result = try api.getAssetsForAlbum(albumId: albumIdArg, updatedTimeCond: updatedTimeCondArg)
|
||||||
|
reply(wrapResult(result))
|
||||||
|
} catch {
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getAssetsForAlbumChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,246 @@
|
|||||||
|
import Photos
|
||||||
|
|
||||||
|
struct AssetWrapper: Hashable, Equatable {
|
||||||
|
let asset: PlatformAsset
|
||||||
|
|
||||||
|
init(with asset: PlatformAsset) {
|
||||||
|
self.asset = asset
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(self.asset.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func == (lhs: AssetWrapper, rhs: AssetWrapper) -> Bool {
|
||||||
|
return lhs.asset.id == rhs.asset.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PHAsset {
|
||||||
|
func toPlatformAsset() -> PlatformAsset {
|
||||||
|
return PlatformAsset(
|
||||||
|
id: localIdentifier,
|
||||||
|
name: title(),
|
||||||
|
type: Int64(mediaType.rawValue),
|
||||||
|
createdAt: creationDate.map { Int64($0.timeIntervalSince1970) },
|
||||||
|
updatedAt: modificationDate.map { Int64($0.timeIntervalSince1970) },
|
||||||
|
durationInSeconds: Int64(duration)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NativeSyncApiImpl: NativeSyncApi {
|
||||||
|
private let defaults: UserDefaults
|
||||||
|
private let changeTokenKey = "immich:changeToken"
|
||||||
|
private let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum]
|
||||||
|
|
||||||
|
init(with defaults: UserDefaults = .standard) {
|
||||||
|
self.defaults = defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 16, *)
|
||||||
|
private func getChangeToken() -> PHPersistentChangeToken? {
|
||||||
|
guard let data = defaults.data(forKey: changeTokenKey) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return try? NSKeyedUnarchiver.unarchivedObject(ofClass: PHPersistentChangeToken.self, from: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 16, *)
|
||||||
|
private func saveChangeToken(token: PHPersistentChangeToken) -> Void {
|
||||||
|
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defaults.set(data, forKey: changeTokenKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearSyncCheckpoint() -> Void {
|
||||||
|
defaults.removeObject(forKey: changeTokenKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkpointSync() {
|
||||||
|
guard #available(iOS 16, *) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
saveChangeToken(token: PHPhotoLibrary.shared().currentChangeToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldFullSync() -> Bool {
|
||||||
|
guard #available(iOS 16, *),
|
||||||
|
PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized,
|
||||||
|
let storedToken = getChangeToken() else {
|
||||||
|
// When we do not have access to photo library, older iOS version or No token available, fallback to full sync
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let _ = try? PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken) else {
|
||||||
|
// Cannot fetch persistent changes
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAlbums() throws -> [PlatformAlbum] {
|
||||||
|
var albums: [PlatformAlbum] = []
|
||||||
|
|
||||||
|
albumTypes.forEach { type in
|
||||||
|
let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil)
|
||||||
|
collections.enumerateObjects { (album, _, _) in
|
||||||
|
let options = PHFetchOptions()
|
||||||
|
options.sortDescriptors = [NSSortDescriptor(key: "modificationDate", ascending: false)]
|
||||||
|
let assets = PHAsset.fetchAssets(in: album, options: options)
|
||||||
|
let isCloud = album.assetCollectionSubtype == .albumCloudShared || album.assetCollectionSubtype == .albumMyPhotoStream
|
||||||
|
|
||||||
|
var domainAlbum = PlatformAlbum(
|
||||||
|
id: album.localIdentifier,
|
||||||
|
name: album.localizedTitle!,
|
||||||
|
updatedAt: nil,
|
||||||
|
isCloud: isCloud,
|
||||||
|
assetCount: Int64(assets.count)
|
||||||
|
)
|
||||||
|
|
||||||
|
if let firstAsset = assets.firstObject {
|
||||||
|
domainAlbum.updatedAt = firstAsset.modificationDate.map { Int64($0.timeIntervalSince1970) }
|
||||||
|
}
|
||||||
|
|
||||||
|
albums.append(domainAlbum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return albums.sorted { $0.id < $1.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMediaChanges() throws -> SyncDelta {
|
||||||
|
guard #available(iOS 16, *) else {
|
||||||
|
throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature requires iOS 16 or later.", details: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else {
|
||||||
|
throw PigeonError(code: "NO_AUTH", message: "No photo library access", details: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let storedToken = getChangeToken() else {
|
||||||
|
// No token exists, definitely need a full sync
|
||||||
|
print("MediaManager::getMediaChanges: No token found")
|
||||||
|
throw PigeonError(code: "NO_TOKEN", message: "No stored change token", details: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentToken = PHPhotoLibrary.shared().currentChangeToken
|
||||||
|
if storedToken == currentToken {
|
||||||
|
return SyncDelta(hasChanges: false, updates: [], deletes: [], assetAlbums: [:])
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let changes = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken)
|
||||||
|
|
||||||
|
var updatedAssets: Set<AssetWrapper> = []
|
||||||
|
var deletedAssets: Set<String> = []
|
||||||
|
|
||||||
|
for change in changes {
|
||||||
|
guard let details = try? change.changeDetails(for: PHObjectType.asset) else { continue }
|
||||||
|
|
||||||
|
let updated = details.updatedLocalIdentifiers.union(details.insertedLocalIdentifiers)
|
||||||
|
deletedAssets.formUnion(details.deletedLocalIdentifiers)
|
||||||
|
|
||||||
|
if (updated.isEmpty) { continue }
|
||||||
|
|
||||||
|
let result = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: nil)
|
||||||
|
for i in 0..<result.count {
|
||||||
|
let asset = result.object(at: i)
|
||||||
|
|
||||||
|
// Asset wrapper only uses the id for comparison. Multiple change can contain the same asset, skip duplicate changes
|
||||||
|
let predicate = PlatformAsset(
|
||||||
|
id: asset.localIdentifier,
|
||||||
|
name: "",
|
||||||
|
type: 0,
|
||||||
|
createdAt: nil,
|
||||||
|
updatedAt: nil,
|
||||||
|
durationInSeconds: 0
|
||||||
|
)
|
||||||
|
if (updatedAssets.contains(AssetWrapper(with: predicate))) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let domainAsset = AssetWrapper(with: asset.toPlatformAsset())
|
||||||
|
updatedAssets.insert(domainAsset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let updates = Array(updatedAssets.map { $0.asset })
|
||||||
|
return SyncDelta(hasChanges: true, updates: updates, deletes: Array(deletedAssets), assetAlbums: buildAssetAlbumsMap(assets: updates))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private func buildAssetAlbumsMap(assets: Array<PlatformAsset>) -> [String: [String]] {
|
||||||
|
guard !assets.isEmpty else {
|
||||||
|
return [:]
|
||||||
|
}
|
||||||
|
|
||||||
|
var albumAssets: [String: [String]] = [:]
|
||||||
|
|
||||||
|
for type in albumTypes {
|
||||||
|
let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil)
|
||||||
|
collections.enumerateObjects { (album, _, _) in
|
||||||
|
let options = PHFetchOptions()
|
||||||
|
options.predicate = NSPredicate(format: "localIdentifier IN %@", assets.map(\.id))
|
||||||
|
let result = PHAsset.fetchAssets(in: album, options: options)
|
||||||
|
result.enumerateObjects { (asset, _, _) in
|
||||||
|
albumAssets[asset.localIdentifier, default: []].append(album.localIdentifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return albumAssets
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAssetIdsForAlbum(albumId: String) throws -> [String] {
|
||||||
|
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
|
||||||
|
guard let album = collections.firstObject else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids: [String] = []
|
||||||
|
let assets = PHAsset.fetchAssets(in: album, options: nil)
|
||||||
|
assets.enumerateObjects { (asset, _, _) in
|
||||||
|
ids.append(asset.localIdentifier)
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 {
|
||||||
|
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
|
||||||
|
guard let album = collections.firstObject else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
let date = NSDate(timeIntervalSince1970: TimeInterval(timestamp))
|
||||||
|
let options = PHFetchOptions()
|
||||||
|
options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date)
|
||||||
|
let assets = PHAsset.fetchAssets(in: album, options: options)
|
||||||
|
return Int64(assets.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] {
|
||||||
|
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
|
||||||
|
guard let album = collections.firstObject else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
let options = PHFetchOptions()
|
||||||
|
if(updatedTimeCond != nil) {
|
||||||
|
let date = NSDate(timeIntervalSince1970: TimeInterval(updatedTimeCond!))
|
||||||
|
options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date)
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = PHAsset.fetchAssets(in: album, options: options)
|
||||||
|
if(result.count == 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
var assets: [PlatformAsset] = []
|
||||||
|
result.enumerateObjects { (asset, _, _) in
|
||||||
|
assets.append(asset.toPlatformAsset())
|
||||||
|
}
|
||||||
|
return assets
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/local_album.model.dart';
|
||||||
|
|
||||||
|
abstract interface class ILocalAlbumRepository implements IDatabaseRepository {
|
||||||
|
Future<List<LocalAlbum>> getAll({SortLocalAlbumsBy? sortBy});
|
||||||
|
|
||||||
|
Future<List<LocalAsset>> getAssetsForAlbum(String albumId);
|
||||||
|
|
||||||
|
Future<List<String>> getAssetIdsForAlbum(String albumId);
|
||||||
|
|
||||||
|
Future<void> upsert(
|
||||||
|
LocalAlbum album, {
|
||||||
|
Iterable<LocalAsset> toUpsert = const [],
|
||||||
|
Iterable<String> toDelete = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> updateAll(Iterable<LocalAlbum> albums);
|
||||||
|
|
||||||
|
Future<void> delete(String albumId);
|
||||||
|
|
||||||
|
Future<void> processDelta({
|
||||||
|
required List<LocalAsset> updates,
|
||||||
|
required List<String> deletes,
|
||||||
|
required Map<String, List<String>> assetAlbums,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> syncAlbumDeletes(
|
||||||
|
String albumId,
|
||||||
|
Iterable<String> assetIdsToKeep,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SortLocalAlbumsBy { id }
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
}''';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,379 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/local_album.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/local_album.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||||
|
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart';
|
||||||
|
import 'package:immich_mobile/utils/diff.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:platform/platform.dart';
|
||||||
|
|
||||||
|
class LocalSyncService {
|
||||||
|
final ILocalAlbumRepository _localAlbumRepository;
|
||||||
|
final NativeSyncApi _nativeSyncApi;
|
||||||
|
final Platform _platform;
|
||||||
|
final StoreService _storeService;
|
||||||
|
final Logger _log = Logger("DeviceSyncService");
|
||||||
|
|
||||||
|
LocalSyncService({
|
||||||
|
required ILocalAlbumRepository localAlbumRepository,
|
||||||
|
required NativeSyncApi nativeSyncApi,
|
||||||
|
required StoreService storeService,
|
||||||
|
Platform? platform,
|
||||||
|
}) : _localAlbumRepository = localAlbumRepository,
|
||||||
|
_nativeSyncApi = nativeSyncApi,
|
||||||
|
_storeService = storeService,
|
||||||
|
_platform = platform ?? const LocalPlatform();
|
||||||
|
|
||||||
|
bool get _ignoreIcloudAssets =>
|
||||||
|
_storeService.get(StoreKey.ignoreIcloudAssets, false) == true;
|
||||||
|
|
||||||
|
Future<void> sync({bool full = false}) async {
|
||||||
|
final Stopwatch stopwatch = Stopwatch()..start();
|
||||||
|
try {
|
||||||
|
if (full || await _nativeSyncApi.shouldFullSync()) {
|
||||||
|
_log.fine("Full sync request from ${full ? "user" : "native"}");
|
||||||
|
DLog.log("Full sync request from ${full ? "user" : "native"}");
|
||||||
|
return await fullSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
final delta = await _nativeSyncApi.getMediaChanges();
|
||||||
|
if (!delta.hasChanges) {
|
||||||
|
_log.fine("No media changes detected. Skipping sync");
|
||||||
|
DLog.log("No media changes detected. Skipping sync");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DLog.log("Delta updated: ${delta.updates.length}");
|
||||||
|
DLog.log("Delta deleted: ${delta.deletes.length}");
|
||||||
|
|
||||||
|
final deviceAlbums = await _nativeSyncApi.getAlbums();
|
||||||
|
await _localAlbumRepository.updateAll(deviceAlbums.toLocalAlbums());
|
||||||
|
await _localAlbumRepository.processDelta(
|
||||||
|
updates: delta.updates.toLocalAssets(),
|
||||||
|
deletes: delta.deletes,
|
||||||
|
assetAlbums: delta.assetAlbums,
|
||||||
|
);
|
||||||
|
|
||||||
|
final dbAlbums = await _localAlbumRepository.getAll();
|
||||||
|
// On Android, we need to sync all albums since it is not possible to
|
||||||
|
// detect album deletions from the native side
|
||||||
|
if (_platform.isAndroid) {
|
||||||
|
for (final album in dbAlbums) {
|
||||||
|
final deviceIds = await _nativeSyncApi.getAssetIdsForAlbum(album.id);
|
||||||
|
await _localAlbumRepository.syncAlbumDeletes(album.id, deviceIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_platform.isIOS) {
|
||||||
|
// On iOS, we need to full sync albums that are marked as cloud as the delta sync
|
||||||
|
// does not include changes for cloud albums. If ignoreIcloudAssets is enabled,
|
||||||
|
// remove the albums from the local database from the previous sync
|
||||||
|
final cloudAlbums =
|
||||||
|
deviceAlbums.where((a) => a.isCloud).toLocalAlbums();
|
||||||
|
for (final album in cloudAlbums) {
|
||||||
|
final dbAlbum = dbAlbums.firstWhereOrNull((a) => a.id == album.id);
|
||||||
|
if (dbAlbum == null) {
|
||||||
|
_log.warning(
|
||||||
|
"Cloud album ${album.name} not found in local database. Skipping sync.",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (_ignoreIcloudAssets) {
|
||||||
|
await removeAlbum(dbAlbum);
|
||||||
|
} else {
|
||||||
|
await updateAlbum(dbAlbum, album);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _nativeSyncApi.checkpointSync();
|
||||||
|
} catch (e, s) {
|
||||||
|
_log.severe("Error performing device sync", e, s);
|
||||||
|
} finally {
|
||||||
|
stopwatch.stop();
|
||||||
|
_log.info("Device sync took - ${stopwatch.elapsedMilliseconds}ms");
|
||||||
|
DLog.log("Device sync took - ${stopwatch.elapsedMilliseconds}ms");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fullSync() async {
|
||||||
|
try {
|
||||||
|
final Stopwatch stopwatch = Stopwatch()..start();
|
||||||
|
|
||||||
|
List<PlatformAlbum> deviceAlbums =
|
||||||
|
List.of(await _nativeSyncApi.getAlbums());
|
||||||
|
if (_platform.isIOS && _ignoreIcloudAssets) {
|
||||||
|
deviceAlbums.removeWhere((album) => album.isCloud);
|
||||||
|
}
|
||||||
|
|
||||||
|
final dbAlbums =
|
||||||
|
await _localAlbumRepository.getAll(sortBy: SortLocalAlbumsBy.id);
|
||||||
|
|
||||||
|
await diffSortedLists(
|
||||||
|
dbAlbums,
|
||||||
|
deviceAlbums.toLocalAlbums(),
|
||||||
|
compare: (a, b) => a.id.compareTo(b.id),
|
||||||
|
both: updateAlbum,
|
||||||
|
onlyFirst: removeAlbum,
|
||||||
|
onlySecond: addAlbum,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _nativeSyncApi.checkpointSync();
|
||||||
|
stopwatch.stop();
|
||||||
|
_log.info("Full device sync took - ${stopwatch.elapsedMilliseconds}ms");
|
||||||
|
DLog.log("Full device sync took - ${stopwatch.elapsedMilliseconds}ms");
|
||||||
|
} catch (e, s) {
|
||||||
|
_log.severe("Error performing full device sync", e, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addAlbum(LocalAlbum album) async {
|
||||||
|
try {
|
||||||
|
_log.fine("Adding device album ${album.name}");
|
||||||
|
|
||||||
|
final assets = album.assetCount > 0
|
||||||
|
? await _nativeSyncApi.getAssetsForAlbum(album.id)
|
||||||
|
: <PlatformAsset>[];
|
||||||
|
|
||||||
|
await _localAlbumRepository.upsert(
|
||||||
|
album,
|
||||||
|
toUpsert: assets.toLocalAssets(),
|
||||||
|
);
|
||||||
|
_log.fine("Successfully added device album ${album.name}");
|
||||||
|
} catch (e, s) {
|
||||||
|
_log.warning("Error while adding device album", e, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeAlbum(LocalAlbum a) async {
|
||||||
|
_log.fine("Removing device album ${a.name}");
|
||||||
|
try {
|
||||||
|
// Asset deletion is handled in the repository
|
||||||
|
await _localAlbumRepository.delete(a.id);
|
||||||
|
} catch (e, s) {
|
||||||
|
_log.warning("Error while removing device album", e, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The deviceAlbum is ignored since we are going to refresh it anyways
|
||||||
|
FutureOr<bool> updateAlbum(LocalAlbum dbAlbum, LocalAlbum deviceAlbum) async {
|
||||||
|
try {
|
||||||
|
_log.fine("Syncing device album ${dbAlbum.name}");
|
||||||
|
|
||||||
|
if (_albumsEqual(deviceAlbum, dbAlbum)) {
|
||||||
|
_log.fine(
|
||||||
|
"Device album ${dbAlbum.name} has not changed. Skipping sync.",
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.fine("Device album ${dbAlbum.name} has changed. Syncing...");
|
||||||
|
|
||||||
|
// Faster path - only new assets added
|
||||||
|
if (await checkAddition(dbAlbum, deviceAlbum)) {
|
||||||
|
_log.fine("Fast synced device album ${dbAlbum.name}");
|
||||||
|
DLog.log("Fast synced device album ${dbAlbum.name}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slower path - full sync
|
||||||
|
return await fullDiff(dbAlbum, deviceAlbum);
|
||||||
|
} catch (e, s) {
|
||||||
|
_log.warning("Error while diff device album", e, s);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
// The [deviceAlbum] is expected to be refreshed before calling this method
|
||||||
|
// with modified time and asset count
|
||||||
|
Future<bool> checkAddition(
|
||||||
|
LocalAlbum dbAlbum,
|
||||||
|
LocalAlbum deviceAlbum,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
_log.fine("Fast syncing device album ${dbAlbum.name}");
|
||||||
|
// Assets has been modified
|
||||||
|
if (deviceAlbum.assetCount <= dbAlbum.assetCount) {
|
||||||
|
_log.fine("Local album has modifications. Proceeding to full sync");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final updatedTime =
|
||||||
|
(dbAlbum.updatedAt.millisecondsSinceEpoch ~/ 1000) + 1;
|
||||||
|
final newAssetsCount =
|
||||||
|
await _nativeSyncApi.getAssetsCountSince(deviceAlbum.id, updatedTime);
|
||||||
|
|
||||||
|
// Early return if no new assets were found
|
||||||
|
if (newAssetsCount == 0) {
|
||||||
|
_log.fine(
|
||||||
|
"No new assets found despite album having changes. Proceeding to full sync for ${dbAlbum.name}",
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether there is only addition or if there has been deletions
|
||||||
|
if (deviceAlbum.assetCount != dbAlbum.assetCount + newAssetsCount) {
|
||||||
|
_log.fine("Local album has modifications. Proceeding to full sync");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final newAssets = await _nativeSyncApi.getAssetsForAlbum(
|
||||||
|
deviceAlbum.id,
|
||||||
|
updatedTimeCond: updatedTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
await _localAlbumRepository.upsert(
|
||||||
|
deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection),
|
||||||
|
toUpsert: newAssets.toLocalAssets(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e, s) {
|
||||||
|
_log.warning("Error on fast syncing local album: ${dbAlbum.name}", e, s);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
// The [deviceAlbum] is expected to be refreshed before calling this method
|
||||||
|
// with modified time and asset count
|
||||||
|
Future<bool> fullDiff(LocalAlbum dbAlbum, LocalAlbum deviceAlbum) async {
|
||||||
|
try {
|
||||||
|
final assetsInDevice = deviceAlbum.assetCount > 0
|
||||||
|
? await _nativeSyncApi
|
||||||
|
.getAssetsForAlbum(deviceAlbum.id)
|
||||||
|
.then((a) => a.toLocalAssets())
|
||||||
|
: <LocalAsset>[];
|
||||||
|
final assetsInDb = dbAlbum.assetCount > 0
|
||||||
|
? await _localAlbumRepository.getAssetsForAlbum(dbAlbum.id)
|
||||||
|
: <LocalAsset>[];
|
||||||
|
|
||||||
|
if (deviceAlbum.assetCount == 0) {
|
||||||
|
_log.fine(
|
||||||
|
"Device album ${deviceAlbum.name} is empty. Removing assets from DB.",
|
||||||
|
);
|
||||||
|
await _localAlbumRepository.upsert(
|
||||||
|
deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection),
|
||||||
|
toDelete: assetsInDb.map((a) => a.id),
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final updatedDeviceAlbum = deviceAlbum.copyWith(
|
||||||
|
backupSelection: dbAlbum.backupSelection,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dbAlbum.assetCount == 0) {
|
||||||
|
_log.fine(
|
||||||
|
"Device album ${deviceAlbum.name} is empty. Adding assets to DB.",
|
||||||
|
);
|
||||||
|
await _localAlbumRepository.upsert(
|
||||||
|
updatedDeviceAlbum,
|
||||||
|
toUpsert: assetsInDevice,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(assetsInDb.isSortedBy((a) => a.id));
|
||||||
|
assetsInDevice.sort((a, b) => a.id.compareTo(b.id));
|
||||||
|
|
||||||
|
final assetsToUpsert = <LocalAsset>[];
|
||||||
|
final assetsToDelete = <String>[];
|
||||||
|
|
||||||
|
diffSortedListsSync(
|
||||||
|
assetsInDb,
|
||||||
|
assetsInDevice,
|
||||||
|
compare: (a, b) => a.id.compareTo(b.id),
|
||||||
|
both: (dbAsset, deviceAsset) {
|
||||||
|
// Custom comparison to check if the asset has been modified without
|
||||||
|
// comparing the checksum
|
||||||
|
if (!_assetsEqual(dbAsset, deviceAsset)) {
|
||||||
|
assetsToUpsert.add(deviceAsset);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
onlyFirst: (dbAsset) => assetsToDelete.add(dbAsset.id),
|
||||||
|
onlySecond: (deviceAsset) => assetsToUpsert.add(deviceAsset),
|
||||||
|
);
|
||||||
|
|
||||||
|
_log.fine(
|
||||||
|
"Syncing ${deviceAlbum.name}. ${assetsToUpsert.length} assets to add/update and ${assetsToDelete.length} assets to delete",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (assetsToUpsert.isEmpty && assetsToDelete.isEmpty) {
|
||||||
|
_log.fine(
|
||||||
|
"No asset changes detected in album ${deviceAlbum.name}. Updating metadata.",
|
||||||
|
);
|
||||||
|
_localAlbumRepository.upsert(updatedDeviceAlbum);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _localAlbumRepository.upsert(
|
||||||
|
updatedDeviceAlbum,
|
||||||
|
toUpsert: assetsToUpsert,
|
||||||
|
toDelete: assetsToDelete,
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e, s) {
|
||||||
|
_log.warning("Error on full syncing local album: ${dbAlbum.name}", e, s);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _assetsEqual(LocalAsset a, LocalAsset b) {
|
||||||
|
return a.updatedAt.isAtSameMomentAs(b.updatedAt) &&
|
||||||
|
a.createdAt.isAtSameMomentAs(b.createdAt) &&
|
||||||
|
a.width == b.width &&
|
||||||
|
a.height == b.height &&
|
||||||
|
a.durationInSeconds == b.durationInSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _albumsEqual(LocalAlbum a, LocalAlbum b) {
|
||||||
|
return a.name == b.name &&
|
||||||
|
a.assetCount == b.assetCount &&
|
||||||
|
a.updatedAt.isAtSameMomentAs(b.updatedAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on Iterable<PlatformAlbum> {
|
||||||
|
List<LocalAlbum> toLocalAlbums() {
|
||||||
|
return map(
|
||||||
|
(e) => LocalAlbum(
|
||||||
|
id: e.id,
|
||||||
|
name: e.name,
|
||||||
|
updatedAt: e.updatedAt == null
|
||||||
|
? DateTime.now()
|
||||||
|
: DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000),
|
||||||
|
assetCount: e.assetCount,
|
||||||
|
),
|
||||||
|
).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on Iterable<PlatformAsset> {
|
||||||
|
List<LocalAsset> toLocalAssets() {
|
||||||
|
return map(
|
||||||
|
(e) => LocalAsset(
|
||||||
|
id: e.id,
|
||||||
|
name: e.name,
|
||||||
|
type: AssetType.values.elementAtOrNull(e.type) ?? AssetType.other,
|
||||||
|
createdAt: e.createdAt == null
|
||||||
|
? DateTime.now()
|
||||||
|
: DateTime.fromMillisecondsSinceEpoch(e.createdAt! * 1000),
|
||||||
|
updatedAt: e.updatedAt == null
|
||||||
|
? DateTime.now()
|
||||||
|
: DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000),
|
||||||
|
durationInSeconds: e.durationInSeconds,
|
||||||
|
),
|
||||||
|
).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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};
|
||||||
|
}
|
||||||
@ -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};
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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};
|
||||||
|
}
|
||||||
@ -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,366 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/local_album.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/local_album.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
import 'package:platform/platform.dart';
|
||||||
|
|
||||||
|
class DriftLocalAlbumRepository extends DriftDatabaseRepository
|
||||||
|
implements ILocalAlbumRepository {
|
||||||
|
final Drift _db;
|
||||||
|
final Platform _platform;
|
||||||
|
const DriftLocalAlbumRepository(this._db, {Platform? platform})
|
||||||
|
: _platform = platform ?? const LocalPlatform(),
|
||||||
|
super(_db);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<LocalAlbum>> getAll({SortLocalAlbumsBy? sortBy}) {
|
||||||
|
final assetCount = _db.localAlbumAssetEntity.assetId.count();
|
||||||
|
|
||||||
|
final query = _db.localAlbumEntity.select().join([
|
||||||
|
leftOuterJoin(
|
||||||
|
_db.localAlbumAssetEntity,
|
||||||
|
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
query
|
||||||
|
..addColumns([assetCount])
|
||||||
|
..groupBy([_db.localAlbumEntity.id]);
|
||||||
|
if (sortBy == SortLocalAlbumsBy.id) {
|
||||||
|
query.orderBy([OrderingTerm.asc(_db.localAlbumEntity.id)]);
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
.map(
|
||||||
|
(row) => row
|
||||||
|
.readTable(_db.localAlbumEntity)
|
||||||
|
.toDto(assetCount: row.read(assetCount) ?? 0),
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> delete(String albumId) => transaction(() async {
|
||||||
|
// Remove all assets that are only in this particular album
|
||||||
|
// We cannot remove all assets in the album because they might be in other albums in iOS
|
||||||
|
// That is not the case on Android since asset <-> album has one:one mapping
|
||||||
|
final assetsToDelete = _platform.isIOS
|
||||||
|
? await _getUniqueAssetsInAlbum(albumId)
|
||||||
|
: await getAssetIdsForAlbum(albumId);
|
||||||
|
await _deleteAssets(assetsToDelete);
|
||||||
|
|
||||||
|
// All the other assets that are still associated will be unlinked automatically on-cascade
|
||||||
|
await _db.managers.localAlbumEntity
|
||||||
|
.filter((a) => a.id.equals(albumId))
|
||||||
|
.delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> syncAlbumDeletes(
|
||||||
|
String albumId,
|
||||||
|
Iterable<String> assetIdsToKeep,
|
||||||
|
) async {
|
||||||
|
if (assetIdsToKeep.isEmpty) {
|
||||||
|
return Future.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
final deleteSmt = _db.localAssetEntity.delete();
|
||||||
|
deleteSmt.where((localAsset) {
|
||||||
|
final subQuery = _db.localAlbumAssetEntity.selectOnly()
|
||||||
|
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||||
|
..join([
|
||||||
|
innerJoin(
|
||||||
|
_db.localAlbumEntity,
|
||||||
|
_db.localAlbumAssetEntity.albumId
|
||||||
|
.equalsExp(_db.localAlbumEntity.id),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
subQuery.where(
|
||||||
|
_db.localAlbumEntity.id.equals(albumId) &
|
||||||
|
_db.localAlbumAssetEntity.assetId.isNotIn(assetIdsToKeep),
|
||||||
|
);
|
||||||
|
return localAsset.id.isInQuery(subQuery);
|
||||||
|
});
|
||||||
|
await deleteSmt.go();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> upsert(
|
||||||
|
LocalAlbum localAlbum, {
|
||||||
|
Iterable<LocalAsset> toUpsert = const [],
|
||||||
|
Iterable<String> toDelete = const [],
|
||||||
|
}) {
|
||||||
|
final companion = LocalAlbumEntityCompanion.insert(
|
||||||
|
id: localAlbum.id,
|
||||||
|
name: localAlbum.name,
|
||||||
|
updatedAt: Value(localAlbum.updatedAt),
|
||||||
|
backupSelection: localAlbum.backupSelection,
|
||||||
|
);
|
||||||
|
|
||||||
|
return _db.transaction(() async {
|
||||||
|
await _db.localAlbumEntity
|
||||||
|
.insertOne(companion, onConflict: DoUpdate((_) => companion));
|
||||||
|
await _addAssets(localAlbum.id, toUpsert);
|
||||||
|
await _removeAssets(localAlbum.id, toDelete);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> updateAll(Iterable<LocalAlbum> albums) {
|
||||||
|
return _db.transaction(() async {
|
||||||
|
await _db.localAlbumEntity
|
||||||
|
.update()
|
||||||
|
.write(const LocalAlbumEntityCompanion(marker_: Value(true)));
|
||||||
|
|
||||||
|
await _db.batch((batch) {
|
||||||
|
for (final album in albums) {
|
||||||
|
final companion = LocalAlbumEntityCompanion.insert(
|
||||||
|
id: album.id,
|
||||||
|
name: album.name,
|
||||||
|
updatedAt: Value(album.updatedAt),
|
||||||
|
backupSelection: album.backupSelection,
|
||||||
|
marker_: const Value(null),
|
||||||
|
);
|
||||||
|
|
||||||
|
batch.insert(
|
||||||
|
_db.localAlbumEntity,
|
||||||
|
companion,
|
||||||
|
onConflict: DoUpdate((_) => companion),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (_platform.isAndroid) {
|
||||||
|
// On Android, an asset can only be in one album
|
||||||
|
// So, get the albums that are marked for deletion
|
||||||
|
// and delete all the assets that are in those albums
|
||||||
|
final deleteSmt = _db.localAssetEntity.delete();
|
||||||
|
deleteSmt.where((localAsset) {
|
||||||
|
final subQuery = _db.localAlbumAssetEntity.selectOnly()
|
||||||
|
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||||
|
..join([
|
||||||
|
innerJoin(
|
||||||
|
_db.localAlbumEntity,
|
||||||
|
_db.localAlbumAssetEntity.albumId
|
||||||
|
.equalsExp(_db.localAlbumEntity.id),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
subQuery.where(_db.localAlbumEntity.marker_.isNotNull());
|
||||||
|
return localAsset.id.isInQuery(subQuery);
|
||||||
|
});
|
||||||
|
await deleteSmt.go();
|
||||||
|
}
|
||||||
|
|
||||||
|
await _db.localAlbumEntity.deleteWhere((f) => f.marker_.isNotNull());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<LocalAsset>> getAssetsForAlbum(String albumId) {
|
||||||
|
final query = _db.localAlbumAssetEntity.select().join(
|
||||||
|
[
|
||||||
|
innerJoin(
|
||||||
|
_db.localAssetEntity,
|
||||||
|
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
|
||||||
|
..orderBy([OrderingTerm.asc(_db.localAssetEntity.id)]);
|
||||||
|
return query
|
||||||
|
.map((row) => row.readTable(_db.localAssetEntity).toDto())
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<String>> getAssetIdsForAlbum(String albumId) {
|
||||||
|
final query = _db.localAlbumAssetEntity.selectOnly()
|
||||||
|
..addColumns([_db.localAlbumAssetEntity.assetId])
|
||||||
|
..where(_db.localAlbumAssetEntity.albumId.equals(albumId));
|
||||||
|
return query
|
||||||
|
.map((row) => row.read(_db.localAlbumAssetEntity.assetId)!)
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> processDelta({
|
||||||
|
required List<LocalAsset> updates,
|
||||||
|
required List<String> deletes,
|
||||||
|
required Map<String, List<String>> assetAlbums,
|
||||||
|
}) {
|
||||||
|
return _db.transaction(() async {
|
||||||
|
await _deleteAssets(deletes);
|
||||||
|
|
||||||
|
await _upsertAssets(updates);
|
||||||
|
// The ugly casting below is required for now because the generated code
|
||||||
|
// casts the returned values from the platform during decoding them
|
||||||
|
// and iterating over them causes the type to be List<Object?> instead of
|
||||||
|
// List<String>
|
||||||
|
await _db.batch((batch) async {
|
||||||
|
assetAlbums.cast<String, List<Object?>>().forEach((assetId, albumIds) {
|
||||||
|
batch.deleteWhere(
|
||||||
|
_db.localAlbumAssetEntity,
|
||||||
|
(f) =>
|
||||||
|
f.albumId.isNotIn(albumIds.cast<String?>().nonNulls) &
|
||||||
|
f.assetId.equals(assetId),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await _db.batch((batch) async {
|
||||||
|
assetAlbums.cast<String, List<Object?>>().forEach((assetId, albumIds) {
|
||||||
|
batch.insertAll(
|
||||||
|
_db.localAlbumAssetEntity,
|
||||||
|
albumIds.cast<String?>().nonNulls.map(
|
||||||
|
(albumId) => LocalAlbumAssetEntityCompanion.insert(
|
||||||
|
assetId: assetId,
|
||||||
|
albumId: albumId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onConflict: DoNothing(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _addAssets(String albumId, Iterable<LocalAsset> assets) {
|
||||||
|
if (assets.isEmpty) {
|
||||||
|
return Future.value();
|
||||||
|
}
|
||||||
|
return transaction(() async {
|
||||||
|
await _upsertAssets(assets);
|
||||||
|
await _db.localAlbumAssetEntity.insertAll(
|
||||||
|
assets.map(
|
||||||
|
(a) => LocalAlbumAssetEntityCompanion.insert(
|
||||||
|
assetId: a.id,
|
||||||
|
albumId: albumId,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
mode: InsertMode.insertOrIgnore,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _removeAssets(String albumId, Iterable<String> assetIds) async {
|
||||||
|
if (assetIds.isEmpty) {
|
||||||
|
return Future.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_platform.isAndroid) {
|
||||||
|
return _deleteAssets(assetIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> assetsToDelete = [];
|
||||||
|
List<String> assetsToUnLink = [];
|
||||||
|
|
||||||
|
final uniqueAssets = await _getUniqueAssetsInAlbum(albumId);
|
||||||
|
if (uniqueAssets.isEmpty) {
|
||||||
|
assetsToUnLink = assetIds.toList();
|
||||||
|
} else {
|
||||||
|
// Delete unique assets and unlink others
|
||||||
|
final uniqueSet = uniqueAssets.toSet();
|
||||||
|
|
||||||
|
for (final assetId in assetIds) {
|
||||||
|
if (uniqueSet.contains(assetId)) {
|
||||||
|
assetsToDelete.add(assetId);
|
||||||
|
} else {
|
||||||
|
assetsToUnLink.add(assetId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return transaction(() async {
|
||||||
|
if (assetsToUnLink.isNotEmpty) {
|
||||||
|
await _db.batch(
|
||||||
|
(batch) => batch.deleteWhere(
|
||||||
|
_db.localAlbumAssetEntity,
|
||||||
|
(f) => f.assetId.isIn(assetsToUnLink) & f.albumId.equals(albumId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _deleteAssets(assetsToDelete);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all asset ids that are only in this album and not in other albums.
|
||||||
|
/// This is useful in cases where the album is a smart album or a user-created album, especially on iOS
|
||||||
|
Future<List<String>> _getUniqueAssetsInAlbum(String albumId) {
|
||||||
|
final assetId = _db.localAlbumAssetEntity.assetId;
|
||||||
|
final query = _db.localAlbumAssetEntity.selectOnly()
|
||||||
|
..addColumns([assetId])
|
||||||
|
..groupBy(
|
||||||
|
[assetId],
|
||||||
|
having: _db.localAlbumAssetEntity.albumId.count().equals(1) &
|
||||||
|
_db.localAlbumAssetEntity.albumId.equals(albumId),
|
||||||
|
);
|
||||||
|
|
||||||
|
return query.map((row) => row.read(assetId)!).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _upsertAssets(Iterable<LocalAsset> localAssets) {
|
||||||
|
if (localAssets.isEmpty) {
|
||||||
|
return Future.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _db.batch((batch) async {
|
||||||
|
batch.insertAllOnConflictUpdate(
|
||||||
|
_db.localAssetEntity,
|
||||||
|
localAssets.map(
|
||||||
|
(a) => LocalAssetEntityCompanion.insert(
|
||||||
|
name: a.name,
|
||||||
|
type: a.type,
|
||||||
|
createdAt: Value(a.createdAt),
|
||||||
|
updatedAt: Value(a.updatedAt),
|
||||||
|
durationInSeconds: Value.absentIfNull(a.durationInSeconds),
|
||||||
|
id: a.id,
|
||||||
|
checksum: Value.absentIfNull(a.checksum),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deleteAssets(Iterable<String> ids) {
|
||||||
|
if (ids.isEmpty) {
|
||||||
|
return Future.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _db.batch(
|
||||||
|
(batch) => batch.deleteWhere(
|
||||||
|
_db.localAssetEntity,
|
||||||
|
(f) => f.id.isIn(ids),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on LocalAlbumEntityData {
|
||||||
|
LocalAlbum toDto({int assetCount = 0}) {
|
||||||
|
return LocalAlbum(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
assetCount: assetCount,
|
||||||
|
backupSelection: backupSelection,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on LocalAssetEntityData {
|
||||||
|
LocalAsset toDto() {
|
||||||
|
return LocalAsset(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
checksum: checksum,
|
||||||
|
type: type,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
durationInSeconds: durationInSeconds,
|
||||||
|
isFavorite: isFavorite,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()();
|
||||||
|
}
|
||||||
@ -0,0 +1,501 @@
|
|||||||
|
// Autogenerated from Pigeon (v25.3.2), do not edit directly.
|
||||||
|
// See also: https://pub.dev/packages/pigeon
|
||||||
|
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
PlatformException _createConnectionError(String channelName) {
|
||||||
|
return PlatformException(
|
||||||
|
code: 'channel-error',
|
||||||
|
message: 'Unable to establish connection on channel: "$channelName".',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _deepEquals(Object? a, Object? b) {
|
||||||
|
if (a is List && b is List) {
|
||||||
|
return a.length == b.length &&
|
||||||
|
a.indexed
|
||||||
|
.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
|
||||||
|
}
|
||||||
|
if (a is Map && b is Map) {
|
||||||
|
return a.length == b.length &&
|
||||||
|
a.entries.every((MapEntry<Object?, Object?> entry) =>
|
||||||
|
(b as Map<Object?, Object?>).containsKey(entry.key) &&
|
||||||
|
_deepEquals(entry.value, b[entry.key]));
|
||||||
|
}
|
||||||
|
return a == b;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlatformAsset {
|
||||||
|
PlatformAsset({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.type,
|
||||||
|
this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
required this.durationInSeconds,
|
||||||
|
});
|
||||||
|
|
||||||
|
String id;
|
||||||
|
|
||||||
|
String name;
|
||||||
|
|
||||||
|
int type;
|
||||||
|
|
||||||
|
int? createdAt;
|
||||||
|
|
||||||
|
int? updatedAt;
|
||||||
|
|
||||||
|
int durationInSeconds;
|
||||||
|
|
||||||
|
List<Object?> _toList() {
|
||||||
|
return <Object?>[
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
durationInSeconds,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
Object encode() {
|
||||||
|
return _toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static PlatformAsset decode(Object result) {
|
||||||
|
result as List<Object?>;
|
||||||
|
return PlatformAsset(
|
||||||
|
id: result[0]! as String,
|
||||||
|
name: result[1]! as String,
|
||||||
|
type: result[2]! as int,
|
||||||
|
createdAt: result[3] as int?,
|
||||||
|
updatedAt: result[4] as int?,
|
||||||
|
durationInSeconds: result[5]! as int,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other is! PlatformAsset || other.runtimeType != runtimeType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (identical(this, other)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return _deepEquals(encode(), other.encode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||||
|
int get hashCode => Object.hashAll(_toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlatformAlbum {
|
||||||
|
PlatformAlbum({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
this.updatedAt,
|
||||||
|
required this.isCloud,
|
||||||
|
required this.assetCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
String id;
|
||||||
|
|
||||||
|
String name;
|
||||||
|
|
||||||
|
int? updatedAt;
|
||||||
|
|
||||||
|
bool isCloud;
|
||||||
|
|
||||||
|
int assetCount;
|
||||||
|
|
||||||
|
List<Object?> _toList() {
|
||||||
|
return <Object?>[
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
updatedAt,
|
||||||
|
isCloud,
|
||||||
|
assetCount,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
Object encode() {
|
||||||
|
return _toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static PlatformAlbum decode(Object result) {
|
||||||
|
result as List<Object?>;
|
||||||
|
return PlatformAlbum(
|
||||||
|
id: result[0]! as String,
|
||||||
|
name: result[1]! as String,
|
||||||
|
updatedAt: result[2] as int?,
|
||||||
|
isCloud: result[3]! as bool,
|
||||||
|
assetCount: result[4]! as int,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other is! PlatformAlbum || other.runtimeType != runtimeType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (identical(this, other)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return _deepEquals(encode(), other.encode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||||
|
int get hashCode => Object.hashAll(_toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
class SyncDelta {
|
||||||
|
SyncDelta({
|
||||||
|
required this.hasChanges,
|
||||||
|
required this.updates,
|
||||||
|
required this.deletes,
|
||||||
|
required this.assetAlbums,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool hasChanges;
|
||||||
|
|
||||||
|
List<PlatformAsset> updates;
|
||||||
|
|
||||||
|
List<String> deletes;
|
||||||
|
|
||||||
|
Map<String, List<String>> assetAlbums;
|
||||||
|
|
||||||
|
List<Object?> _toList() {
|
||||||
|
return <Object?>[
|
||||||
|
hasChanges,
|
||||||
|
updates,
|
||||||
|
deletes,
|
||||||
|
assetAlbums,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
Object encode() {
|
||||||
|
return _toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static SyncDelta decode(Object result) {
|
||||||
|
result as List<Object?>;
|
||||||
|
return SyncDelta(
|
||||||
|
hasChanges: result[0]! as bool,
|
||||||
|
updates: (result[1] as List<Object?>?)!.cast<PlatformAsset>(),
|
||||||
|
deletes: (result[2] as List<Object?>?)!.cast<String>(),
|
||||||
|
assetAlbums:
|
||||||
|
(result[3] as Map<Object?, Object?>?)!.cast<String, List<String>>(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other is! SyncDelta || other.runtimeType != runtimeType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (identical(this, other)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return _deepEquals(encode(), other.encode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||||
|
int get hashCode => Object.hashAll(_toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PigeonCodec extends StandardMessageCodec {
|
||||||
|
const _PigeonCodec();
|
||||||
|
@override
|
||||||
|
void writeValue(WriteBuffer buffer, Object? value) {
|
||||||
|
if (value is int) {
|
||||||
|
buffer.putUint8(4);
|
||||||
|
buffer.putInt64(value);
|
||||||
|
} else if (value is PlatformAsset) {
|
||||||
|
buffer.putUint8(129);
|
||||||
|
writeValue(buffer, value.encode());
|
||||||
|
} else if (value is PlatformAlbum) {
|
||||||
|
buffer.putUint8(130);
|
||||||
|
writeValue(buffer, value.encode());
|
||||||
|
} else if (value is SyncDelta) {
|
||||||
|
buffer.putUint8(131);
|
||||||
|
writeValue(buffer, value.encode());
|
||||||
|
} else {
|
||||||
|
super.writeValue(buffer, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Object? readValueOfType(int type, ReadBuffer buffer) {
|
||||||
|
switch (type) {
|
||||||
|
case 129:
|
||||||
|
return PlatformAsset.decode(readValue(buffer)!);
|
||||||
|
case 130:
|
||||||
|
return PlatformAlbum.decode(readValue(buffer)!);
|
||||||
|
case 131:
|
||||||
|
return SyncDelta.decode(readValue(buffer)!);
|
||||||
|
default:
|
||||||
|
return super.readValueOfType(type, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NativeSyncApi {
|
||||||
|
/// Constructor for [NativeSyncApi]. The [binaryMessenger] named argument is
|
||||||
|
/// available for dependency injection. If it is left null, the default
|
||||||
|
/// BinaryMessenger will be used which routes to the host platform.
|
||||||
|
NativeSyncApi(
|
||||||
|
{BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
||||||
|
: pigeonVar_binaryMessenger = binaryMessenger,
|
||||||
|
pigeonVar_messageChannelSuffix =
|
||||||
|
messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||||
|
final BinaryMessenger? pigeonVar_binaryMessenger;
|
||||||
|
|
||||||
|
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||||
|
|
||||||
|
final String pigeonVar_messageChannelSuffix;
|
||||||
|
|
||||||
|
Future<bool> shouldFullSync() async {
|
||||||
|
final String pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$pigeonVar_messageChannelSuffix';
|
||||||
|
final BasicMessageChannel<Object?> pigeonVar_channel =
|
||||||
|
BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||||
|
final List<Object?>? pigeonVar_replyList =
|
||||||
|
await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
if (pigeonVar_replyList == null) {
|
||||||
|
throw _createConnectionError(pigeonVar_channelName);
|
||||||
|
} else if (pigeonVar_replyList.length > 1) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: pigeonVar_replyList[0]! as String,
|
||||||
|
message: pigeonVar_replyList[1] as String?,
|
||||||
|
details: pigeonVar_replyList[2],
|
||||||
|
);
|
||||||
|
} else if (pigeonVar_replyList[0] == null) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: 'null-error',
|
||||||
|
message: 'Host platform returned null value for non-null return value.',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (pigeonVar_replyList[0] as bool?)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SyncDelta> getMediaChanges() async {
|
||||||
|
final String pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$pigeonVar_messageChannelSuffix';
|
||||||
|
final BasicMessageChannel<Object?> pigeonVar_channel =
|
||||||
|
BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||||
|
final List<Object?>? pigeonVar_replyList =
|
||||||
|
await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
if (pigeonVar_replyList == null) {
|
||||||
|
throw _createConnectionError(pigeonVar_channelName);
|
||||||
|
} else if (pigeonVar_replyList.length > 1) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: pigeonVar_replyList[0]! as String,
|
||||||
|
message: pigeonVar_replyList[1] as String?,
|
||||||
|
details: pigeonVar_replyList[2],
|
||||||
|
);
|
||||||
|
} else if (pigeonVar_replyList[0] == null) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: 'null-error',
|
||||||
|
message: 'Host platform returned null value for non-null return value.',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (pigeonVar_replyList[0] as SyncDelta?)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> checkpointSync() async {
|
||||||
|
final String pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync$pigeonVar_messageChannelSuffix';
|
||||||
|
final BasicMessageChannel<Object?> pigeonVar_channel =
|
||||||
|
BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||||
|
final List<Object?>? pigeonVar_replyList =
|
||||||
|
await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
if (pigeonVar_replyList == null) {
|
||||||
|
throw _createConnectionError(pigeonVar_channelName);
|
||||||
|
} else if (pigeonVar_replyList.length > 1) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: pigeonVar_replyList[0]! as String,
|
||||||
|
message: pigeonVar_replyList[1] as String?,
|
||||||
|
details: pigeonVar_replyList[2],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearSyncCheckpoint() async {
|
||||||
|
final String pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint$pigeonVar_messageChannelSuffix';
|
||||||
|
final BasicMessageChannel<Object?> pigeonVar_channel =
|
||||||
|
BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||||
|
final List<Object?>? pigeonVar_replyList =
|
||||||
|
await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
if (pigeonVar_replyList == null) {
|
||||||
|
throw _createConnectionError(pigeonVar_channelName);
|
||||||
|
} else if (pigeonVar_replyList.length > 1) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: pigeonVar_replyList[0]! as String,
|
||||||
|
message: pigeonVar_replyList[1] as String?,
|
||||||
|
details: pigeonVar_replyList[2],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> getAssetIdsForAlbum(String albumId) async {
|
||||||
|
final String pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum$pigeonVar_messageChannelSuffix';
|
||||||
|
final BasicMessageChannel<Object?> pigeonVar_channel =
|
||||||
|
BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture =
|
||||||
|
pigeonVar_channel.send(<Object?>[albumId]);
|
||||||
|
final List<Object?>? pigeonVar_replyList =
|
||||||
|
await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
if (pigeonVar_replyList == null) {
|
||||||
|
throw _createConnectionError(pigeonVar_channelName);
|
||||||
|
} else if (pigeonVar_replyList.length > 1) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: pigeonVar_replyList[0]! as String,
|
||||||
|
message: pigeonVar_replyList[1] as String?,
|
||||||
|
details: pigeonVar_replyList[2],
|
||||||
|
);
|
||||||
|
} else if (pigeonVar_replyList[0] == null) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: 'null-error',
|
||||||
|
message: 'Host platform returned null value for non-null return value.',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<String>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<PlatformAlbum>> getAlbums() async {
|
||||||
|
final String pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$pigeonVar_messageChannelSuffix';
|
||||||
|
final BasicMessageChannel<Object?> pigeonVar_channel =
|
||||||
|
BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||||
|
final List<Object?>? pigeonVar_replyList =
|
||||||
|
await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
if (pigeonVar_replyList == null) {
|
||||||
|
throw _createConnectionError(pigeonVar_channelName);
|
||||||
|
} else if (pigeonVar_replyList.length > 1) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: pigeonVar_replyList[0]! as String,
|
||||||
|
message: pigeonVar_replyList[1] as String?,
|
||||||
|
details: pigeonVar_replyList[2],
|
||||||
|
);
|
||||||
|
} else if (pigeonVar_replyList[0] == null) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: 'null-error',
|
||||||
|
message: 'Host platform returned null value for non-null return value.',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<PlatformAlbum>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getAssetsCountSince(String albumId, int timestamp) async {
|
||||||
|
final String pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince$pigeonVar_messageChannelSuffix';
|
||||||
|
final BasicMessageChannel<Object?> pigeonVar_channel =
|
||||||
|
BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture =
|
||||||
|
pigeonVar_channel.send(<Object?>[albumId, timestamp]);
|
||||||
|
final List<Object?>? pigeonVar_replyList =
|
||||||
|
await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
if (pigeonVar_replyList == null) {
|
||||||
|
throw _createConnectionError(pigeonVar_channelName);
|
||||||
|
} else if (pigeonVar_replyList.length > 1) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: pigeonVar_replyList[0]! as String,
|
||||||
|
message: pigeonVar_replyList[1] as String?,
|
||||||
|
details: pigeonVar_replyList[2],
|
||||||
|
);
|
||||||
|
} else if (pigeonVar_replyList[0] == null) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: 'null-error',
|
||||||
|
message: 'Host platform returned null value for non-null return value.',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (pigeonVar_replyList[0] as int?)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<PlatformAsset>> getAssetsForAlbum(String albumId,
|
||||||
|
{int? updatedTimeCond}) async {
|
||||||
|
final String pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$pigeonVar_messageChannelSuffix';
|
||||||
|
final BasicMessageChannel<Object?> pigeonVar_channel =
|
||||||
|
BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture =
|
||||||
|
pigeonVar_channel.send(<Object?>[albumId, updatedTimeCond]);
|
||||||
|
final List<Object?>? pigeonVar_replyList =
|
||||||
|
await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
if (pigeonVar_replyList == null) {
|
||||||
|
throw _createConnectionError(pigeonVar_channelName);
|
||||||
|
} else if (pigeonVar_replyList.length > 1) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: pigeonVar_replyList[0]! as String,
|
||||||
|
message: pigeonVar_replyList[1] as String?,
|
||||||
|
details: pigeonVar_replyList[2],
|
||||||
|
);
|
||||||
|
} else if (pigeonVar_replyList[0] == null) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: 'null-error',
|
||||||
|
message: 'Host platform returned null value for non-null return value.',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<PlatformAsset>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/log.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||||
|
// ignore: import_rule_isar
|
||||||
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
|
const kDevLoggerTag = 'DEV';
|
||||||
|
|
||||||
|
abstract final class DLog {
|
||||||
|
const DLog();
|
||||||
|
|
||||||
|
static Stream<List<LogMessage>> watchLog() {
|
||||||
|
final db = Isar.getInstance();
|
||||||
|
if (db == null) {
|
||||||
|
debugPrint('Isar is not initialized');
|
||||||
|
return const Stream.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.loggerMessages
|
||||||
|
.filter()
|
||||||
|
.context1EqualTo(kDevLoggerTag)
|
||||||
|
.sortByCreatedAtDesc()
|
||||||
|
.watch(fireImmediately: true)
|
||||||
|
.map((logs) => logs.map((log) => log.toDto()).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void clearLog() {
|
||||||
|
final db = Isar.getInstance();
|
||||||
|
if (db == null) {
|
||||||
|
debugPrint('Isar is not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
db.writeTxnSync(() {
|
||||||
|
db.loggerMessages.filter().context1EqualTo(kDevLoggerTag).deleteAllSync();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log(String message, [Object? error, StackTrace? stackTrace]) {
|
||||||
|
debugPrint('[$kDevLoggerTag] [${DateTime.now()}] $message');
|
||||||
|
if (error != null) {
|
||||||
|
debugPrint('Error: $error');
|
||||||
|
}
|
||||||
|
if (stackTrace != null) {
|
||||||
|
debugPrint('StackTrace: $stackTrace');
|
||||||
|
}
|
||||||
|
|
||||||
|
final isar = Isar.getInstance();
|
||||||
|
if (isar == null) {
|
||||||
|
debugPrint('Isar is not initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final record = LogMessage(
|
||||||
|
message: message,
|
||||||
|
level: LogLevel.info,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
logger: kDevLoggerTag,
|
||||||
|
error: error?.toString(),
|
||||||
|
stack: stackTrace?.toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
unawaited(IsarLogRepository(isar).insert(record));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,174 @@
|
|||||||
|
// ignore_for_file: avoid-local-functions
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:drift/drift.dart' hide Column;
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart';
|
||||||
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
|
final _features = [
|
||||||
|
_Feature(
|
||||||
|
name: 'Sync Local',
|
||||||
|
icon: Icons.photo_album_rounded,
|
||||||
|
onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(),
|
||||||
|
),
|
||||||
|
_Feature(
|
||||||
|
name: 'Sync Local Full',
|
||||||
|
icon: Icons.photo_library_rounded,
|
||||||
|
onTap: (_, ref) => ref.read(backgroundSyncProvider).syncLocal(full: true),
|
||||||
|
),
|
||||||
|
_Feature(
|
||||||
|
name: 'Sync Remote',
|
||||||
|
icon: Icons.refresh_rounded,
|
||||||
|
onTap: (_, ref) => ref.read(backgroundSyncProvider).syncRemote(),
|
||||||
|
),
|
||||||
|
_Feature(
|
||||||
|
name: 'WAL Checkpoint',
|
||||||
|
icon: Icons.save_rounded,
|
||||||
|
onTap: (_, ref) => ref
|
||||||
|
.read(driftProvider)
|
||||||
|
.customStatement("pragma wal_checkpoint(truncate)"),
|
||||||
|
),
|
||||||
|
_Feature(
|
||||||
|
name: 'Clear Delta Checkpoint',
|
||||||
|
icon: Icons.delete_rounded,
|
||||||
|
onTap: (_, ref) => ref.read(nativeSyncApiProvider).clearSyncCheckpoint(),
|
||||||
|
),
|
||||||
|
_Feature(
|
||||||
|
name: 'Clear Local Data',
|
||||||
|
icon: Icons.delete_forever_rounded,
|
||||||
|
onTap: (_, ref) async {
|
||||||
|
final db = ref.read(driftProvider);
|
||||||
|
await db.localAssetEntity.deleteAll();
|
||||||
|
await db.localAlbumEntity.deleteAll();
|
||||||
|
await db.localAlbumAssetEntity.deleteAll();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_Feature(
|
||||||
|
name: 'Local Media Summary',
|
||||||
|
icon: Icons.table_chart_rounded,
|
||||||
|
onTap: (ctx, _) => ctx.pushRoute(const LocalMediaSummaryRoute()),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class FeatInDevPage extends StatelessWidget {
|
||||||
|
const FeatInDevPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Features in Development'),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
flex: 1,
|
||||||
|
child: ListView.builder(
|
||||||
|
itemBuilder: (_, index) {
|
||||||
|
final feat = _features[index];
|
||||||
|
return Consumer(
|
||||||
|
builder: (ctx, ref, _) => ListTile(
|
||||||
|
title: Text(feat.name),
|
||||||
|
trailing: Icon(feat.icon),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
onTap: () => unawaited(feat.onTap(ctx, ref)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
itemCount: _features.length,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 0),
|
||||||
|
const Flexible(child: _DevLogs()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Feature {
|
||||||
|
const _Feature({
|
||||||
|
required this.name,
|
||||||
|
required this.icon,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final IconData icon;
|
||||||
|
final Future<void> Function(BuildContext, WidgetRef _) onTap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore: prefer-single-widget-per-file
|
||||||
|
class _DevLogs extends StatelessWidget {
|
||||||
|
const _DevLogs();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: DLog.clearLog,
|
||||||
|
icon: Icon(
|
||||||
|
Icons.delete_outline_rounded,
|
||||||
|
size: 20.0,
|
||||||
|
color: context.primaryColor,
|
||||||
|
semanticLabel: "Clear logs",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: StreamBuilder(
|
||||||
|
initialData: [],
|
||||||
|
stream: DLog.watchLog(),
|
||||||
|
builder: (_, logMessages) {
|
||||||
|
return ListView.separated(
|
||||||
|
itemBuilder: (ctx, index) {
|
||||||
|
// ignore: avoid-unsafe-collection-methods
|
||||||
|
final logMessage = logMessages.data![index];
|
||||||
|
return ListTile(
|
||||||
|
title: Text(
|
||||||
|
logMessage.message,
|
||||||
|
style: TextStyle(
|
||||||
|
color: ctx.colorScheme.onSurface,
|
||||||
|
fontSize: 14.0,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
"at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)}",
|
||||||
|
style: TextStyle(
|
||||||
|
color: ctx.colorScheme.onSurfaceSecondary,
|
||||||
|
fontSize: 12.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dense: true,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
tileColor: Colors.transparent,
|
||||||
|
minLeadingWidth: 10,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
separatorBuilder: (_, index) {
|
||||||
|
return const Divider(height: 0);
|
||||||
|
},
|
||||||
|
itemCount: logMessages.data?.length ?? 0,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/local_album.interface.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
|
|
||||||
|
final localAlbumRepository = Provider<ILocalAlbumRepository>(
|
||||||
|
(ref) => DriftLocalAlbumRepository(ref.watch(driftProvider)),
|
||||||
|
);
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||||
|
|
||||||
|
final nativeSyncApiProvider = Provider<NativeSyncApi>((_) => NativeSyncApi());
|
||||||
@ -0,0 +1,89 @@
|
|||||||
|
import 'package:pigeon/pigeon.dart';
|
||||||
|
|
||||||
|
@ConfigurePigeon(
|
||||||
|
PigeonOptions(
|
||||||
|
dartOut: 'lib/platform/native_sync_api.g.dart',
|
||||||
|
swiftOut: 'ios/Runner/Sync/Messages.g.swift',
|
||||||
|
swiftOptions: SwiftOptions(),
|
||||||
|
kotlinOut:
|
||||||
|
'android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt',
|
||||||
|
kotlinOptions: KotlinOptions(package: 'app.alextran.immich.sync'),
|
||||||
|
dartOptions: DartOptions(),
|
||||||
|
dartPackageName: 'immich_mobile',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
class PlatformAsset {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
// Follows AssetType enum from base_asset.model.dart
|
||||||
|
final int type;
|
||||||
|
// Seconds since epoch
|
||||||
|
final int? createdAt;
|
||||||
|
final int? updatedAt;
|
||||||
|
final int durationInSeconds;
|
||||||
|
|
||||||
|
const PlatformAsset({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.type,
|
||||||
|
this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
this.durationInSeconds = 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlatformAlbum {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
// Seconds since epoch
|
||||||
|
final int? updatedAt;
|
||||||
|
final bool isCloud;
|
||||||
|
final int assetCount;
|
||||||
|
|
||||||
|
const PlatformAlbum({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
this.updatedAt,
|
||||||
|
this.isCloud = false,
|
||||||
|
this.assetCount = 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SyncDelta {
|
||||||
|
final bool hasChanges;
|
||||||
|
final List<PlatformAsset> updates;
|
||||||
|
final List<String> deletes;
|
||||||
|
// Asset -> Album mapping
|
||||||
|
final Map<String, List<String>> assetAlbums;
|
||||||
|
|
||||||
|
const SyncDelta({
|
||||||
|
this.hasChanges = false,
|
||||||
|
this.updates = const [],
|
||||||
|
this.deletes = const [],
|
||||||
|
this.assetAlbums = const {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostApi()
|
||||||
|
abstract class NativeSyncApi {
|
||||||
|
bool shouldFullSync();
|
||||||
|
|
||||||
|
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
|
||||||
|
SyncDelta getMediaChanges();
|
||||||
|
|
||||||
|
void checkpointSync();
|
||||||
|
|
||||||
|
void clearSyncCheckpoint();
|
||||||
|
|
||||||
|
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
|
||||||
|
List<String> getAssetIdsForAlbum(String albumId);
|
||||||
|
|
||||||
|
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
|
||||||
|
List<PlatformAlbum> getAlbums();
|
||||||
|
|
||||||
|
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
|
||||||
|
int getAssetsCountSince(String albumId, int timestamp);
|
||||||
|
|
||||||
|
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
|
||||||
|
List<PlatformAsset> getAssetsForAlbum(String albumId, {int? updatedTimeCond});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue