mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
feat: native sync ios
This commit is contained in:
parent
dbe1a127c9
commit
214893d2f4
@ -0,0 +1,278 @@
|
||||
// Autogenerated from Pigeon (v25.3.1), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||
|
||||
|
||||
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 Asset (
|
||||
val id: String,
|
||||
val name: String,
|
||||
val type: Long,
|
||||
val createdAt: String? = null,
|
||||
val updatedAt: String? = null,
|
||||
val durationInSeconds: Long,
|
||||
val albumIds: List<String>
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
fun fromList(pigeonVar_list: List<Any?>): Asset {
|
||||
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 String?
|
||||
val updatedAt = pigeonVar_list[4] as String?
|
||||
val durationInSeconds = pigeonVar_list[5] as Long
|
||||
val albumIds = pigeonVar_list[6] as List<String>
|
||||
return Asset(id, name, type, createdAt, updatedAt, durationInSeconds, albumIds)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
return listOf(
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
durationInSeconds,
|
||||
albumIds,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is Asset) {
|
||||
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 updates: List<Asset>,
|
||||
val deletes: List<String>
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
fun fromList(pigeonVar_list: List<Any?>): SyncDelta {
|
||||
val updates = pigeonVar_list[0] as List<Asset>
|
||||
val deletes = pigeonVar_list[1] as List<String>
|
||||
return SyncDelta(updates, deletes)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
return listOf(
|
||||
updates,
|
||||
deletes,
|
||||
)
|
||||
}
|
||||
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 {
|
||||
Asset.fromList(it)
|
||||
}
|
||||
}
|
||||
130.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 Asset -> {
|
||||
stream.write(129)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is SyncDelta -> {
|
||||
stream.write(130)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
else -> super.writeValue(stream, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||
interface ImHostService {
|
||||
fun shouldFullSync(callback: (Result<Boolean>) -> Unit)
|
||||
fun hasMediaChanges(callback: (Result<Boolean>) -> Unit)
|
||||
fun getMediaChanges(callback: (Result<SyncDelta>) -> Unit)
|
||||
fun checkpointSync(callback: (Result<Unit>) -> Unit)
|
||||
|
||||
companion object {
|
||||
/** The codec used by ImHostService. */
|
||||
val codec: MessageCodec<Any?> by lazy {
|
||||
messagesPigeonCodec()
|
||||
}
|
||||
/** Sets up an instance of `ImHostService` to handle messages through the `binaryMessenger`. */
|
||||
@JvmOverloads
|
||||
fun setUp(binaryMessenger: BinaryMessenger, api: ImHostService?, messageChannelSuffix: String = "") {
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
val taskQueue = binaryMessenger.makeBackgroundTaskQueue()
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostService.shouldFullSync$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
api.shouldFullSync{ result: Result<Boolean> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
||||
} else {
|
||||
val data = result.getOrNull()
|
||||
reply.reply(MessagesPigeonUtils.wrapResult(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostService.hasMediaChanges$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
api.hasMediaChanges{ result: Result<Boolean> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
||||
} else {
|
||||
val data = result.getOrNull()
|
||||
reply.reply(MessagesPigeonUtils.wrapResult(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostService.getMediaChanges$separatedMessageChannelSuffix", codec, taskQueue)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
api.getMediaChanges{ result: Result<SyncDelta> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
||||
} else {
|
||||
val data = result.getOrNull()
|
||||
reply.reply(MessagesPigeonUtils.wrapResult(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostService.checkpointSync$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
api.checkpointSync{ result: Result<Unit> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
||||
} else {
|
||||
reply.reply(MessagesPigeonUtils.wrapResult(null))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
2
mobile/drift_schemas/main/drift_schema_v1.json
generated
2
mobile/drift_schemas/main/drift_schema_v1.json
generated
File diff suppressed because one or more lines are too long
@ -5,31 +5,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
|
||||
sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "76.0.0"
|
||||
_macros:
|
||||
dependency: transitive
|
||||
description: dart
|
||||
source: sdk
|
||||
version: "0.3.3"
|
||||
version: "80.0.0"
|
||||
analyzer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
|
||||
sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.11.0"
|
||||
version: "7.3.0"
|
||||
analyzer_plugin:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: analyzer_plugin
|
||||
sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161"
|
||||
sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.3"
|
||||
version: "0.13.0"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -106,34 +101,42 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: custom_lint
|
||||
sha256: "4500e88854e7581ee43586abeaf4443cb22375d6d289241a87b1aadf678d5545"
|
||||
sha256: "409c485fd14f544af1da965d5a0d160ee57cd58b63eeaa7280a4f28cf5bda7f1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.10"
|
||||
version: "0.7.5"
|
||||
custom_lint_builder:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: custom_lint_builder
|
||||
sha256: "5a95eff100da256fbf086b329c17c8b49058c261cdf56d3a4157d3c31c511d78"
|
||||
sha256: "107e0a43606138015777590ee8ce32f26ba7415c25b722ff0908a6f5d7a4c228"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.10"
|
||||
version: "0.7.5"
|
||||
custom_lint_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: custom_lint_core
|
||||
sha256: "76a4046cc71d976222a078a8fd4a65e198b70545a8d690a75196dd14f08510f6"
|
||||
sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.10"
|
||||
version: "0.7.5"
|
||||
custom_lint_visitor:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: custom_lint_visitor
|
||||
sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0+7.3.0"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820"
|
||||
sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.8"
|
||||
version: "3.0.1"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -154,10 +157,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: freezed_annotation
|
||||
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
|
||||
sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
version: "3.0.0"
|
||||
glob:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -198,14 +201,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
macros:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: macros
|
||||
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3-main.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -5,9 +5,9 @@ environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
analyzer: ^6.0.0
|
||||
analyzer_plugin: ^0.11.3
|
||||
custom_lint_builder: ^0.6.4
|
||||
analyzer: ^7.0.0
|
||||
analyzer_plugin: ^0.13.0
|
||||
custom_lint_builder: ^0.7.5
|
||||
glob: ^2.1.2
|
||||
|
||||
dev_dependencies:
|
||||
|
@ -89,6 +89,20 @@
|
||||
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
B28F36282DC3150F00B18015 /* Platform */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = Platform;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
@ -175,6 +189,7 @@
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B28F36282DC3150F00B18015 /* Platform */,
|
||||
FA9973382CF6DF4B000EF859 /* Runner.entitlements */,
|
||||
65DD438629917FAD0047FFA8 /* BackgroundSync */,
|
||||
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */,
|
||||
@ -224,6 +239,9 @@
|
||||
dependencies = (
|
||||
FAC6F8992D287C890078CB2F /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
B28F36282DC3150F00B18015 /* Platform */,
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Immich-Debug.app */;
|
||||
|
@ -22,6 +22,10 @@ import UIKit
|
||||
BackgroundServicePlugin.registerBackgroundProcessing()
|
||||
|
||||
BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!)
|
||||
|
||||
// Register pigeon handler
|
||||
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
|
||||
ImHostServiceSetup.setUp(binaryMessenger: controller.binaryMessenger, api: ImHostServiceImpl())
|
||||
|
||||
BackgroundServicePlugin.setPluginRegistrantCallback { registry in
|
||||
if !registry.hasPlugin("org.cocoapods.path-provider-foundation") {
|
||||
|
152
mobile/ios/Runner/Platform/MediaManager.swift
Normal file
152
mobile/ios/Runner/Platform/MediaManager.swift
Normal file
@ -0,0 +1,152 @@
|
||||
import Photos
|
||||
|
||||
class MediaManager {
|
||||
let _defaults: UserDefaults
|
||||
|
||||
let _changeTokenKey = "immich:changeToken";
|
||||
|
||||
init(with defaults: UserDefaults = .standard) {
|
||||
_defaults = defaults
|
||||
}
|
||||
|
||||
@available(iOS 16, *)
|
||||
func _getChangeToken() -> PHPersistentChangeToken? {
|
||||
guard let encodedToken = _defaults.data(forKey: _changeTokenKey) else {
|
||||
print("_getChangeToken: Change token not available in UserDefaults")
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
let changeToken = try NSKeyedUnarchiver.unarchivedObject(ofClass: PHPersistentChangeToken.self, from: encodedToken)
|
||||
return changeToken
|
||||
} catch {
|
||||
print("_getChangeToken: Cannot decode the token from UserDefaults")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16, *)
|
||||
func _saveChangeToken(token: PHPersistentChangeToken) -> Void {
|
||||
do {
|
||||
let encodedToken = try NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true)
|
||||
_defaults.set(encodedToken, forKey: _changeTokenKey)
|
||||
print("_setChangeToken: Change token saved to UserDefaults")
|
||||
} catch {
|
||||
print("_setChangeToken: Failed to persist the token to UserDefaults: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16, *)
|
||||
func checkpointSync(completion: @escaping (Result<Void, any Error>) -> Void) {
|
||||
_saveChangeToken(token: PHPhotoLibrary.shared().currentChangeToken)
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
@available(iOS 16, *)
|
||||
func shouldFullSync(completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||
guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else {
|
||||
// When we do not have access to photo library, return true to fallback to old sync
|
||||
completion(.success(true))
|
||||
return
|
||||
}
|
||||
|
||||
guard let storedToken = _getChangeToken() else {
|
||||
// No token exists, perform the initial full sync
|
||||
print("shouldUseOldSync: No token found")
|
||||
completion(.success(true))
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
_ = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken)
|
||||
completion(.success(false))
|
||||
} catch {
|
||||
// fallback to using old sync when we cannot detect changes using the available token
|
||||
print("shouldUseOldSync: fetchPersistentChanges failed with error (\(error))")
|
||||
completion(.success(true))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@available(iOS 16, *)
|
||||
func hasMediaChanges(completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||
guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else {
|
||||
completion(.failure(PigeonError(code: "1", message: "No photo library access", details: nil)))
|
||||
return
|
||||
}
|
||||
|
||||
let storedToken = _getChangeToken()
|
||||
let currentToken = PHPhotoLibrary.shared().currentChangeToken
|
||||
completion(.success(storedToken != currentToken))
|
||||
}
|
||||
|
||||
@available(iOS 16, *)
|
||||
func getMediaChanges(completion: @escaping (Result<SyncDelta, Error>) -> Void) {
|
||||
guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else {
|
||||
completion(.failure(PigeonError(code: "1", message: "No photo library access", details: nil)))
|
||||
return
|
||||
}
|
||||
|
||||
guard let storedToken = _getChangeToken() else {
|
||||
// No token exists, definitely need a full sync
|
||||
print("getMediaChanges: No token found")
|
||||
completion(.failure(PigeonError(code: "2", message: "No stored change token", details: nil)))
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let result = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken)
|
||||
|
||||
let dateFormatter = ISO8601DateFormatter()
|
||||
dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
var delta = SyncDelta(updates: [], deletes: [])
|
||||
for changes in result {
|
||||
let details = try changes.changeDetails(for: PHObjectType.asset)
|
||||
let updated = details.updatedLocalIdentifiers.union(details.insertedLocalIdentifiers)
|
||||
let deleted = details.deletedLocalIdentifiers
|
||||
|
||||
let options = PHFetchOptions()
|
||||
options.includeHiddenAssets = true
|
||||
let updatedAssets = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: options)
|
||||
|
||||
var updates: [Asset] = []
|
||||
updatedAssets.enumerateObjects { (asset, _, _) in
|
||||
let id = asset.localIdentifier
|
||||
let name = PHAssetResource.assetResources(for: asset).first?.originalFilename ?? asset.title()
|
||||
let type: Int64 = Int64(asset.mediaType.rawValue)
|
||||
let createdAt = asset.creationDate.map { dateFormatter.string(from: $0) }
|
||||
let updatedAt = asset.modificationDate.map { dateFormatter.string(from: $0) }
|
||||
let durationInSeconds: Int64 = Int64(asset.duration)
|
||||
|
||||
let dAsset = Asset(id: id, name: name, type: type, createdAt: createdAt, updatedAt: updatedAt, durationInSeconds: durationInSeconds, albumIds: self._getAlbumIdsForAsset(asset: asset))
|
||||
updates.append(dAsset)
|
||||
}
|
||||
|
||||
delta.updates.append(contentsOf: updates)
|
||||
delta.deletes.append(contentsOf: deleted)
|
||||
}
|
||||
|
||||
completion(.success(delta))
|
||||
return
|
||||
} catch {
|
||||
print("getMediaChanges: Error fetching persistent changes: \(error)")
|
||||
completion(.failure(PigeonError(code: "3", message: error.localizedDescription, details: nil)))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16, *)
|
||||
func _getAlbumIdsForAsset(asset: PHAsset) -> [String] {
|
||||
var albumIds: [String] = []
|
||||
var albums = PHAssetCollection.fetchAssetCollectionsContaining(asset, with: .album, options: nil)
|
||||
albums.enumerateObjects { (album, _, _) in
|
||||
albumIds.append(album.localIdentifier)
|
||||
}
|
||||
albums = PHAssetCollection.fetchAssetCollectionsContaining(asset, with: .smartAlbum, options: nil)
|
||||
albums.enumerateObjects { (album, _, _) in
|
||||
albumIds.append(album.localIdentifier)
|
||||
}
|
||||
return albumIds
|
||||
}
|
||||
|
||||
}
|
333
mobile/ios/Runner/Platform/Messages.g.swift
Normal file
333
mobile/ios/Runner/Platform/Messages.g.swift
Normal file
@ -0,0 +1,333 @@
|
||||
// Autogenerated from Pigeon (v25.3.1), 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 Asset: Hashable {
|
||||
var id: String
|
||||
var name: String
|
||||
var type: Int64
|
||||
var createdAt: String? = nil
|
||||
var updatedAt: String? = nil
|
||||
var durationInSeconds: Int64
|
||||
var albumIds: [String]
|
||||
|
||||
|
||||
// swift-format-ignore: AlwaysUseLowerCamelCase
|
||||
static func fromList(_ pigeonVar_list: [Any?]) -> Asset? {
|
||||
let id = pigeonVar_list[0] as! String
|
||||
let name = pigeonVar_list[1] as! String
|
||||
let type = pigeonVar_list[2] as! Int64
|
||||
let createdAt: String? = nilOrValue(pigeonVar_list[3])
|
||||
let updatedAt: String? = nilOrValue(pigeonVar_list[4])
|
||||
let durationInSeconds = pigeonVar_list[5] as! Int64
|
||||
let albumIds = pigeonVar_list[6] as! [String]
|
||||
|
||||
return Asset(
|
||||
id: id,
|
||||
name: name,
|
||||
type: type,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
durationInSeconds: durationInSeconds,
|
||||
albumIds: albumIds
|
||||
)
|
||||
}
|
||||
func toList() -> [Any?] {
|
||||
return [
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
durationInSeconds,
|
||||
albumIds,
|
||||
]
|
||||
}
|
||||
static func == (lhs: Asset, rhs: Asset) -> 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 updates: [Asset]
|
||||
var deletes: [String]
|
||||
|
||||
|
||||
// swift-format-ignore: AlwaysUseLowerCamelCase
|
||||
static func fromList(_ pigeonVar_list: [Any?]) -> SyncDelta? {
|
||||
let updates = pigeonVar_list[0] as! [Asset]
|
||||
let deletes = pigeonVar_list[1] as! [String]
|
||||
|
||||
return SyncDelta(
|
||||
updates: updates,
|
||||
deletes: deletes
|
||||
)
|
||||
}
|
||||
func toList() -> [Any?] {
|
||||
return [
|
||||
updates,
|
||||
deletes,
|
||||
]
|
||||
}
|
||||
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 Asset.fromList(self.readValue() as! [Any?])
|
||||
case 130:
|
||||
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? Asset {
|
||||
super.writeByte(129)
|
||||
super.writeValue(value.toList())
|
||||
} else if let value = value as? SyncDelta {
|
||||
super.writeByte(130)
|
||||
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 ImHostService {
|
||||
func shouldFullSync(completion: @escaping (Result<Bool, Error>) -> Void)
|
||||
func hasMediaChanges(completion: @escaping (Result<Bool, Error>) -> Void)
|
||||
func getMediaChanges(completion: @escaping (Result<SyncDelta, Error>) -> Void)
|
||||
func checkpointSync(completion: @escaping (Result<Void, Error>) -> Void)
|
||||
}
|
||||
|
||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||
class ImHostServiceSetup {
|
||||
static var codec: FlutterStandardMessageCodec { MessagesPigeonCodec.shared }
|
||||
/// Sets up an instance of `ImHostService` to handle messages through the `binaryMessenger`.
|
||||
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: ImHostService?, 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.ImHostService.shouldFullSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
shouldFullSyncChannel.setMessageHandler { _, reply in
|
||||
api.shouldFullSync { result in
|
||||
switch result {
|
||||
case .success(let res):
|
||||
reply(wrapResult(res))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
shouldFullSyncChannel.setMessageHandler(nil)
|
||||
}
|
||||
let hasMediaChangesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostService.hasMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
hasMediaChangesChannel.setMessageHandler { _, reply in
|
||||
api.hasMediaChanges { result in
|
||||
switch result {
|
||||
case .success(let res):
|
||||
reply(wrapResult(res))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
hasMediaChangesChannel.setMessageHandler(nil)
|
||||
}
|
||||
let getMediaChangesChannel = taskQueue == nil
|
||||
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostService.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostService.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
||||
if let api = api {
|
||||
getMediaChangesChannel.setMessageHandler { _, reply in
|
||||
api.getMediaChanges { result in
|
||||
switch result {
|
||||
case .success(let res):
|
||||
reply(wrapResult(res))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
getMediaChangesChannel.setMessageHandler(nil)
|
||||
}
|
||||
let checkpointSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostService.checkpointSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
checkpointSyncChannel.setMessageHandler { _, reply in
|
||||
api.checkpointSync { result in
|
||||
switch result {
|
||||
case .success:
|
||||
reply(wrapResult(nil))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
checkpointSyncChannel.setMessageHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
44
mobile/ios/Runner/Platform/MessagesImpl.swift
Normal file
44
mobile/ios/Runner/Platform/MessagesImpl.swift
Normal file
@ -0,0 +1,44 @@
|
||||
import Photos
|
||||
|
||||
class ImHostServiceImpl: ImHostService {
|
||||
let _mediaManager: MediaManager
|
||||
|
||||
init() {
|
||||
_mediaManager = MediaManager()
|
||||
}
|
||||
|
||||
func shouldFullSync(completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||
if #available(iOS 16, *) {
|
||||
_mediaManager.shouldFullSync(completion: completion)
|
||||
return;
|
||||
} else {
|
||||
// Always fall back to full sync on older iOS versions
|
||||
completion(.success(true))
|
||||
}
|
||||
}
|
||||
|
||||
func hasMediaChanges(completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||
if #available(iOS 16, *) {
|
||||
_mediaManager.hasMediaChanges(completion: completion)
|
||||
} else {
|
||||
completion(.failure(PigeonError(code: "-1", message: "Not supported", details: nil)))
|
||||
}
|
||||
}
|
||||
|
||||
func getMediaChanges(completion: @escaping (Result<SyncDelta, Error>) -> Void) {
|
||||
if #available(iOS 16, *) {
|
||||
_mediaManager.getMediaChanges(completion: completion)
|
||||
} else {
|
||||
completion(.failure(PigeonError(code: "-1", message: "Not supported", details: nil)))
|
||||
}
|
||||
}
|
||||
|
||||
func checkpointSync(completion: @escaping (Result<Void, any Error>) -> Void) {
|
||||
if #available(iOS 16, *) {
|
||||
_mediaManager.checkpointSync(completion: completion)
|
||||
} else {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/local_album.model.dart';
|
||||
import 'package:immich_mobile/platform/messages.g.dart';
|
||||
|
||||
abstract interface class ILocalAlbumRepository implements IDatabaseRepository {
|
||||
Future<void> insert(LocalAlbum album, Iterable<LocalAsset> assets);
|
||||
@ -13,6 +14,10 @@ abstract interface class ILocalAlbumRepository implements IDatabaseRepository {
|
||||
|
||||
Future<void> update(LocalAlbum album);
|
||||
|
||||
Future<void> updateAll(Iterable<LocalAlbum> albums);
|
||||
|
||||
Future<void> handleSyncDelta(SyncDelta delta);
|
||||
|
||||
Future<void> delete(String albumId);
|
||||
|
||||
Future<void> removeAssets(String albumId, Iterable<String> assetIds);
|
||||
|
@ -2,5 +2,5 @@ import 'package:immich_mobile/domain/interfaces/db.interface.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
|
||||
abstract interface class ILocalAssetRepository implements IDatabaseRepository {
|
||||
Future<LocalAsset> get(String assetId);
|
||||
Future<LocalAsset> get(String id);
|
||||
}
|
||||
|
@ -4,28 +4,50 @@ import 'package:collection/collection.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/album_media.interface.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/local_album.interface.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/local_asset.interface.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/local_album.model.dart';
|
||||
import 'package:immich_mobile/platform/messages.g.dart' as platform;
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
import 'package:immich_mobile/utils/nullable_value.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class DeviceSyncService {
|
||||
final IAlbumMediaRepository _albumMediaRepository;
|
||||
final ILocalAlbumRepository _localAlbumRepository;
|
||||
final ILocalAssetRepository _localAssetRepository;
|
||||
final platform.ImHostService _hostService;
|
||||
final Logger _log = Logger("SyncService");
|
||||
|
||||
DeviceSyncService({
|
||||
required IAlbumMediaRepository albumMediaRepository,
|
||||
required ILocalAlbumRepository localAlbumRepository,
|
||||
required ILocalAssetRepository localAssetRepository,
|
||||
required platform.ImHostService hostService,
|
||||
}) : _albumMediaRepository = albumMediaRepository,
|
||||
_localAlbumRepository = localAlbumRepository,
|
||||
_localAssetRepository = localAssetRepository;
|
||||
_hostService = hostService;
|
||||
|
||||
Future<void> sync() async {
|
||||
try {
|
||||
if (await _hostService.shouldFullSync()) {
|
||||
_log.fine("Cannot use partial sync. Performing full sync");
|
||||
return await fullSync();
|
||||
}
|
||||
|
||||
if (!await _hostService.hasMediaChanges()) {
|
||||
_log.fine("No media changes detected. Skipping sync");
|
||||
return;
|
||||
}
|
||||
|
||||
final deviceAlbums = await _albumMediaRepository.getAll();
|
||||
await _localAlbumRepository.updateAll(deviceAlbums);
|
||||
|
||||
final delta = await _hostService.getMediaChanges();
|
||||
await _localAlbumRepository.handleSyncDelta(delta);
|
||||
await _hostService.checkpointSync();
|
||||
} catch (e, s) {
|
||||
_log.severe("Error performing device sync", e, s);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fullSync() async {
|
||||
try {
|
||||
final Stopwatch stopwatch = Stopwatch()..start();
|
||||
// The deviceAlbums will not have the updatedAt field
|
||||
@ -47,6 +69,7 @@ class DeviceSyncService {
|
||||
onlySecond: addAlbum,
|
||||
);
|
||||
|
||||
_hostService.checkpointSync();
|
||||
stopwatch.stop();
|
||||
_log.info("Full device sync took - ${stopwatch.elapsedMilliseconds}ms");
|
||||
} catch (e, s) {
|
||||
@ -57,17 +80,12 @@ class DeviceSyncService {
|
||||
Future<void> addAlbum(LocalAlbum newAlbum) async {
|
||||
try {
|
||||
_log.info("Adding device album ${newAlbum.name}");
|
||||
final deviceAlbum = await _albumMediaRepository.refresh(newAlbum.id);
|
||||
final album = await _albumMediaRepository.refresh(newAlbum.id);
|
||||
|
||||
final assets = deviceAlbum.assetCount > 0
|
||||
? await _albumMediaRepository.getAssetsForAlbum(deviceAlbum.id)
|
||||
final assets = album.assetCount > 0
|
||||
? await _albumMediaRepository.getAssetsForAlbum(album.id)
|
||||
: <LocalAsset>[];
|
||||
|
||||
final album = deviceAlbum.copyWith(
|
||||
// The below assumes the list is already sorted by createdDate from the filter
|
||||
thumbnailId: NullableValue.valueOrEmpty(assets.firstOrNull?.id),
|
||||
);
|
||||
|
||||
await _localAlbumRepository.insert(album, assets);
|
||||
_log.fine("Successfully added device album ${album.name}");
|
||||
} catch (e, s) {
|
||||
@ -110,7 +128,7 @@ class DeviceSyncService {
|
||||
}
|
||||
|
||||
// Slower path - full sync
|
||||
return await fullSync(dbAlbum, deviceAlbum);
|
||||
return await fullDiff(dbAlbum, deviceAlbum);
|
||||
} catch (e, s) {
|
||||
_log.warning("Error while diff device album", e, s);
|
||||
}
|
||||
@ -155,25 +173,8 @@ class DeviceSyncService {
|
||||
return false;
|
||||
}
|
||||
|
||||
String? thumbnailId = dbAlbum.thumbnailId;
|
||||
if (thumbnailId == null || newAssets.isNotEmpty) {
|
||||
if (thumbnailId == null) {
|
||||
thumbnailId = newAssets.firstOrNull?.id;
|
||||
} else if (newAssets.isNotEmpty) {
|
||||
// The below assumes the list is already sorted by createdDate from the filter
|
||||
final oldThumbAsset = await _localAssetRepository.get(thumbnailId);
|
||||
if (oldThumbAsset.createdAt
|
||||
.isBefore(newAssets.firstOrNull!.createdAt)) {
|
||||
thumbnailId = newAssets.firstOrNull?.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await _updateAlbum(
|
||||
deviceAlbum.copyWith(
|
||||
thumbnailId: NullableValue.valueOrEmpty(thumbnailId),
|
||||
backupSelection: dbAlbum.backupSelection,
|
||||
),
|
||||
deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection),
|
||||
assetsToUpsert: newAssets,
|
||||
);
|
||||
|
||||
@ -187,7 +188,7 @@ class DeviceSyncService {
|
||||
@visibleForTesting
|
||||
// The [deviceAlbum] is expected to be refreshed before calling this method
|
||||
// with modified time and asset count
|
||||
Future<bool> fullSync(LocalAlbum dbAlbum, LocalAlbum deviceAlbum) async {
|
||||
Future<bool> fullDiff(LocalAlbum dbAlbum, LocalAlbum deviceAlbum) async {
|
||||
try {
|
||||
final assetsInDevice = deviceAlbum.assetCount > 0
|
||||
? await _albumMediaRepository.getAssetsForAlbum(deviceAlbum.id)
|
||||
@ -201,23 +202,13 @@ class DeviceSyncService {
|
||||
"Device album ${deviceAlbum.name} is empty. Removing assets from DB.",
|
||||
);
|
||||
await _updateAlbum(
|
||||
deviceAlbum.copyWith(
|
||||
// Clear thumbnail for empty album
|
||||
thumbnailId: const NullableValue.empty(),
|
||||
backupSelection: dbAlbum.backupSelection,
|
||||
),
|
||||
deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection),
|
||||
assetIdsToDelete: assetsInDb.map((a) => a.id),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// The below assumes the list is already sorted by createdDate from the filter
|
||||
String? thumbnailId = assetsInDevice.isNotEmpty
|
||||
? assetsInDevice.firstOrNull?.id
|
||||
: dbAlbum.thumbnailId;
|
||||
|
||||
final updatedDeviceAlbum = deviceAlbum.copyWith(
|
||||
thumbnailId: NullableValue.valueOrEmpty(thumbnailId),
|
||||
backupSelection: dbAlbum.backupSelection,
|
||||
);
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'package:drift/drift.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_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
class LocalAlbumEntity extends Table with DriftDefaultsMixin {
|
||||
@ -10,10 +9,8 @@ class LocalAlbumEntity extends Table with DriftDefaultsMixin {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get name => text()();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
TextColumn get thumbnailId => text()
|
||||
.nullable()
|
||||
.references(LocalAssetEntity, #localId, onDelete: KeyAction.setNull)();
|
||||
IntColumn get backupSelection => intEnum<BackupSelection>()();
|
||||
BoolColumn get marker_ => boolean().withDefault(const Constant(false))();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
@ -26,7 +23,6 @@ extension LocalAlbumEntityX on LocalAlbumEntityData {
|
||||
name: name,
|
||||
updatedAt: updatedAt,
|
||||
assetCount: assetCount,
|
||||
thumbnailId: thumbnailId,
|
||||
backupSelection: backupSelection,
|
||||
);
|
||||
}
|
||||
|
@ -7,59 +7,24 @@ 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;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
|
||||
as i5;
|
||||
import 'package:drift/internal/modular.dart' as i6;
|
||||
|
||||
typedef $$LocalAlbumEntityTableCreateCompanionBuilder
|
||||
= i1.LocalAlbumEntityCompanion Function({
|
||||
required String id,
|
||||
required String name,
|
||||
i0.Value<DateTime> updatedAt,
|
||||
i0.Value<String?> thumbnailId,
|
||||
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<String?> thumbnailId,
|
||||
i0.Value<i2.BackupSelection> backupSelection,
|
||||
i0.Value<bool> marker_,
|
||||
});
|
||||
|
||||
final class $$LocalAlbumEntityTableReferences extends i0.BaseReferences<
|
||||
i0.GeneratedDatabase, i1.$LocalAlbumEntityTable, i1.LocalAlbumEntityData> {
|
||||
$$LocalAlbumEntityTableReferences(
|
||||
super.$_db, super.$_table, super.$_typedResult);
|
||||
|
||||
static i5.$LocalAssetEntityTable _thumbnailIdTable(i0.GeneratedDatabase db) =>
|
||||
i6.ReadDatabaseContainer(db)
|
||||
.resultSet<i5.$LocalAssetEntityTable>('local_asset_entity')
|
||||
.createAlias(i0.$_aliasNameGenerator(
|
||||
i6.ReadDatabaseContainer(db)
|
||||
.resultSet<i1.$LocalAlbumEntityTable>('local_album_entity')
|
||||
.thumbnailId,
|
||||
i6.ReadDatabaseContainer(db)
|
||||
.resultSet<i5.$LocalAssetEntityTable>('local_asset_entity')
|
||||
.localId));
|
||||
|
||||
i5.$$LocalAssetEntityTableProcessedTableManager? get thumbnailId {
|
||||
final $_column = $_itemColumn<String>('thumbnail_id');
|
||||
if ($_column == null) return null;
|
||||
final manager = i5
|
||||
.$$LocalAssetEntityTableTableManager(
|
||||
$_db,
|
||||
i6.ReadDatabaseContainer($_db)
|
||||
.resultSet<i5.$LocalAssetEntityTable>('local_asset_entity'))
|
||||
.filter((f) => f.localId.sqlEquals($_column));
|
||||
final item = $_typedResult.readTableOrNull(_thumbnailIdTable($_db));
|
||||
if (item == null) return manager;
|
||||
return i0.ProcessedTableManager(
|
||||
manager.$state.copyWith(prefetchedData: [item]));
|
||||
}
|
||||
}
|
||||
|
||||
class $$LocalAlbumEntityTableFilterComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAlbumEntityTable> {
|
||||
$$LocalAlbumEntityTableFilterComposer({
|
||||
@ -83,27 +48,8 @@ class $$LocalAlbumEntityTableFilterComposer
|
||||
column: $table.backupSelection,
|
||||
builder: (column) => i0.ColumnWithTypeConverterFilters(column));
|
||||
|
||||
i5.$$LocalAssetEntityTableFilterComposer get thumbnailId {
|
||||
final i5.$$LocalAssetEntityTableFilterComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.thumbnailId,
|
||||
referencedTable: i6.ReadDatabaseContainer($db)
|
||||
.resultSet<i5.$LocalAssetEntityTable>('local_asset_entity'),
|
||||
getReferencedColumn: (t) => t.localId,
|
||||
builder: (joinBuilder,
|
||||
{$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer}) =>
|
||||
i5.$$LocalAssetEntityTableFilterComposer(
|
||||
$db: $db,
|
||||
$table: i6.ReadDatabaseContainer($db)
|
||||
.resultSet<i5.$LocalAssetEntityTable>('local_asset_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
));
|
||||
return composer;
|
||||
}
|
||||
i0.ColumnFilters<bool> get marker_ => $composableBuilder(
|
||||
column: $table.marker_, builder: (column) => i0.ColumnFilters(column));
|
||||
}
|
||||
|
||||
class $$LocalAlbumEntityTableOrderingComposer
|
||||
@ -129,29 +75,8 @@ class $$LocalAlbumEntityTableOrderingComposer
|
||||
column: $table.backupSelection,
|
||||
builder: (column) => i0.ColumnOrderings(column));
|
||||
|
||||
i5.$$LocalAssetEntityTableOrderingComposer get thumbnailId {
|
||||
final i5.$$LocalAssetEntityTableOrderingComposer composer =
|
||||
$composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.thumbnailId,
|
||||
referencedTable: i6.ReadDatabaseContainer($db)
|
||||
.resultSet<i5.$LocalAssetEntityTable>('local_asset_entity'),
|
||||
getReferencedColumn: (t) => t.localId,
|
||||
builder: (joinBuilder,
|
||||
{$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer}) =>
|
||||
i5.$$LocalAssetEntityTableOrderingComposer(
|
||||
$db: $db,
|
||||
$table: i6.ReadDatabaseContainer($db)
|
||||
.resultSet<i5.$LocalAssetEntityTable>(
|
||||
'local_asset_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
));
|
||||
return composer;
|
||||
}
|
||||
i0.ColumnOrderings<bool> get marker_ => $composableBuilder(
|
||||
column: $table.marker_, builder: (column) => i0.ColumnOrderings(column));
|
||||
}
|
||||
|
||||
class $$LocalAlbumEntityTableAnnotationComposer
|
||||
@ -176,29 +101,8 @@ class $$LocalAlbumEntityTableAnnotationComposer
|
||||
get backupSelection => $composableBuilder(
|
||||
column: $table.backupSelection, builder: (column) => column);
|
||||
|
||||
i5.$$LocalAssetEntityTableAnnotationComposer get thumbnailId {
|
||||
final i5.$$LocalAssetEntityTableAnnotationComposer composer =
|
||||
$composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.thumbnailId,
|
||||
referencedTable: i6.ReadDatabaseContainer($db)
|
||||
.resultSet<i5.$LocalAssetEntityTable>('local_asset_entity'),
|
||||
getReferencedColumn: (t) => t.localId,
|
||||
builder: (joinBuilder,
|
||||
{$addJoinBuilderToRootComposer,
|
||||
$removeJoinBuilderFromRootComposer}) =>
|
||||
i5.$$LocalAssetEntityTableAnnotationComposer(
|
||||
$db: $db,
|
||||
$table: i6.ReadDatabaseContainer($db)
|
||||
.resultSet<i5.$LocalAssetEntityTable>(
|
||||
'local_asset_entity'),
|
||||
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||
joinBuilder: joinBuilder,
|
||||
$removeJoinBuilderFromRootComposer:
|
||||
$removeJoinBuilderFromRootComposer,
|
||||
));
|
||||
return composer;
|
||||
}
|
||||
i0.GeneratedColumn<bool> get marker_ =>
|
||||
$composableBuilder(column: $table.marker_, builder: (column) => column);
|
||||
}
|
||||
|
||||
class $$LocalAlbumEntityTableTableManager extends i0.RootTableManager<
|
||||
@ -210,9 +114,13 @@ class $$LocalAlbumEntityTableTableManager extends i0.RootTableManager<
|
||||
i1.$$LocalAlbumEntityTableAnnotationComposer,
|
||||
$$LocalAlbumEntityTableCreateCompanionBuilder,
|
||||
$$LocalAlbumEntityTableUpdateCompanionBuilder,
|
||||
(i1.LocalAlbumEntityData, i1.$$LocalAlbumEntityTableReferences),
|
||||
(
|
||||
i1.LocalAlbumEntityData,
|
||||
i0.BaseReferences<i0.GeneratedDatabase, i1.$LocalAlbumEntityTable,
|
||||
i1.LocalAlbumEntityData>
|
||||
),
|
||||
i1.LocalAlbumEntityData,
|
||||
i0.PrefetchHooks Function({bool thumbnailId})> {
|
||||
i0.PrefetchHooks Function()> {
|
||||
$$LocalAlbumEntityTableTableManager(
|
||||
i0.GeneratedDatabase db, i1.$LocalAlbumEntityTable table)
|
||||
: super(i0.TableManagerState(
|
||||
@ -229,73 +137,35 @@ class $$LocalAlbumEntityTableTableManager extends i0.RootTableManager<
|
||||
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<String?> thumbnailId = 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,
|
||||
thumbnailId: thumbnailId,
|
||||
backupSelection: backupSelection,
|
||||
marker_: marker_,
|
||||
),
|
||||
createCompanionCallback: ({
|
||||
required String id,
|
||||
required String name,
|
||||
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
||||
i0.Value<String?> thumbnailId = 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,
|
||||
thumbnailId: thumbnailId,
|
||||
backupSelection: backupSelection,
|
||||
marker_: marker_,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map((e) => (
|
||||
e.readTable(table),
|
||||
i1.$$LocalAlbumEntityTableReferences(db, table, e)
|
||||
))
|
||||
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
|
||||
.toList(),
|
||||
prefetchHooksCallback: ({thumbnailId = 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 (thumbnailId) {
|
||||
state = state.withJoin(
|
||||
currentTable: table,
|
||||
currentColumn: table.thumbnailId,
|
||||
referencedTable: i1.$$LocalAlbumEntityTableReferences
|
||||
._thumbnailIdTable(db),
|
||||
referencedColumn: i1.$$LocalAlbumEntityTableReferences
|
||||
._thumbnailIdTable(db)
|
||||
.localId,
|
||||
) as T;
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
getPrefetchedDataCallback: (items) async {
|
||||
return [];
|
||||
},
|
||||
);
|
||||
},
|
||||
prefetchHooksCallback: null,
|
||||
));
|
||||
}
|
||||
|
||||
@ -308,9 +178,13 @@ typedef $$LocalAlbumEntityTableProcessedTableManager = i0.ProcessedTableManager<
|
||||
i1.$$LocalAlbumEntityTableAnnotationComposer,
|
||||
$$LocalAlbumEntityTableCreateCompanionBuilder,
|
||||
$$LocalAlbumEntityTableUpdateCompanionBuilder,
|
||||
(i1.LocalAlbumEntityData, i1.$$LocalAlbumEntityTableReferences),
|
||||
(
|
||||
i1.LocalAlbumEntityData,
|
||||
i0.BaseReferences<i0.GeneratedDatabase, i1.$LocalAlbumEntityTable,
|
||||
i1.LocalAlbumEntityData>
|
||||
),
|
||||
i1.LocalAlbumEntityData,
|
||||
i0.PrefetchHooks Function({bool thumbnailId})>;
|
||||
i0.PrefetchHooks Function()>;
|
||||
|
||||
class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
|
||||
with i0.TableInfo<$LocalAlbumEntityTable, i1.LocalAlbumEntityData> {
|
||||
@ -337,15 +211,6 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
|
||||
type: i0.DriftSqlType.dateTime,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: i4.currentDateAndTime);
|
||||
static const i0.VerificationMeta _thumbnailIdMeta =
|
||||
const i0.VerificationMeta('thumbnailId');
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> thumbnailId =
|
||||
i0.GeneratedColumn<String>('thumbnail_id', aliasedName, true,
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||
'REFERENCES local_asset_entity (local_id) ON DELETE SET NULL'));
|
||||
@override
|
||||
late final i0.GeneratedColumnWithTypeConverter<i2.BackupSelection, int>
|
||||
backupSelection = i0.GeneratedColumn<int>(
|
||||
@ -353,9 +218,19 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
|
||||
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, false,
|
||||
type: i0.DriftSqlType.bool,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints:
|
||||
i0.GeneratedColumn.constraintIsAlways('CHECK ("marker" IN (0, 1))'),
|
||||
defaultValue: const i4.Constant(false));
|
||||
@override
|
||||
List<i0.GeneratedColumn> get $columns =>
|
||||
[id, name, updatedAt, thumbnailId, backupSelection];
|
||||
[id, name, updatedAt, backupSelection, marker_];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
@ -382,11 +257,9 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
|
||||
context.handle(_updatedAtMeta,
|
||||
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta));
|
||||
}
|
||||
if (data.containsKey('thumbnail_id')) {
|
||||
context.handle(
|
||||
_thumbnailIdMeta,
|
||||
thumbnailId.isAcceptableOrUnknown(
|
||||
data['thumbnail_id']!, _thumbnailIdMeta));
|
||||
if (data.containsKey('marker')) {
|
||||
context.handle(_marker_Meta,
|
||||
marker_.isAcceptableOrUnknown(data['marker']!, _marker_Meta));
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@ -404,11 +277,11 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
|
||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!,
|
||||
updatedAt: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!,
|
||||
thumbnailId: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}thumbnail_id']),
|
||||
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'])!,
|
||||
);
|
||||
}
|
||||
|
||||
@ -432,28 +305,26 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||
final String id;
|
||||
final String name;
|
||||
final DateTime updatedAt;
|
||||
final String? thumbnailId;
|
||||
final i2.BackupSelection backupSelection;
|
||||
final bool marker_;
|
||||
const LocalAlbumEntityData(
|
||||
{required this.id,
|
||||
required this.name,
|
||||
required this.updatedAt,
|
||||
this.thumbnailId,
|
||||
required this.backupSelection});
|
||||
required this.backupSelection,
|
||||
required 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);
|
||||
if (!nullToAbsent || thumbnailId != null) {
|
||||
map['thumbnail_id'] = i0.Variable<String>(thumbnailId);
|
||||
}
|
||||
{
|
||||
map['backup_selection'] = i0.Variable<int>(i1
|
||||
.$LocalAlbumEntityTable.$converterbackupSelection
|
||||
.toSql(backupSelection));
|
||||
}
|
||||
map['marker'] = i0.Variable<bool>(marker_);
|
||||
return map;
|
||||
}
|
||||
|
||||
@ -464,9 +335,9 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||
id: serializer.fromJson<String>(json['id']),
|
||||
name: serializer.fromJson<String>(json['name']),
|
||||
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
|
||||
thumbnailId: serializer.fromJson<String?>(json['thumbnailId']),
|
||||
backupSelection: i1.$LocalAlbumEntityTable.$converterbackupSelection
|
||||
.fromJson(serializer.fromJson<int>(json['backupSelection'])),
|
||||
marker_: serializer.fromJson<bool>(json['marker_']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
@ -476,10 +347,10 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||
'id': serializer.toJson<String>(id),
|
||||
'name': serializer.toJson<String>(name),
|
||||
'updatedAt': serializer.toJson<DateTime>(updatedAt),
|
||||
'thumbnailId': serializer.toJson<String?>(thumbnailId),
|
||||
'backupSelection': serializer.toJson<int>(i1
|
||||
.$LocalAlbumEntityTable.$converterbackupSelection
|
||||
.toJson(backupSelection)),
|
||||
'marker_': serializer.toJson<bool>(marker_),
|
||||
};
|
||||
}
|
||||
|
||||
@ -487,25 +358,24 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||
{String? id,
|
||||
String? name,
|
||||
DateTime? updatedAt,
|
||||
i0.Value<String?> thumbnailId = const i0.Value.absent(),
|
||||
i2.BackupSelection? backupSelection}) =>
|
||||
i2.BackupSelection? backupSelection,
|
||||
bool? marker_}) =>
|
||||
i1.LocalAlbumEntityData(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
thumbnailId: thumbnailId.present ? thumbnailId.value : this.thumbnailId,
|
||||
backupSelection: backupSelection ?? this.backupSelection,
|
||||
marker_: marker_ ?? 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,
|
||||
thumbnailId:
|
||||
data.thumbnailId.present ? data.thumbnailId.value : this.thumbnailId,
|
||||
backupSelection: data.backupSelection.present
|
||||
? data.backupSelection.value
|
||||
: this.backupSelection,
|
||||
marker_: data.marker_.present ? data.marker_.value : this.marker_,
|
||||
);
|
||||
}
|
||||
|
||||
@ -515,15 +385,15 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||
..write('id: $id, ')
|
||||
..write('name: $name, ')
|
||||
..write('updatedAt: $updatedAt, ')
|
||||
..write('thumbnailId: $thumbnailId, ')
|
||||
..write('backupSelection: $backupSelection')
|
||||
..write('backupSelection: $backupSelection, ')
|
||||
..write('marker_: $marker_')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(id, name, updatedAt, thumbnailId, backupSelection);
|
||||
Object.hash(id, name, updatedAt, backupSelection, marker_);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
@ -531,8 +401,8 @@ class LocalAlbumEntityData extends i0.DataClass
|
||||
other.id == this.id &&
|
||||
other.name == this.name &&
|
||||
other.updatedAt == this.updatedAt &&
|
||||
other.thumbnailId == this.thumbnailId &&
|
||||
other.backupSelection == this.backupSelection);
|
||||
other.backupSelection == this.backupSelection &&
|
||||
other.marker_ == this.marker_);
|
||||
}
|
||||
|
||||
class LocalAlbumEntityCompanion
|
||||
@ -540,21 +410,21 @@ class LocalAlbumEntityCompanion
|
||||
final i0.Value<String> id;
|
||||
final i0.Value<String> name;
|
||||
final i0.Value<DateTime> updatedAt;
|
||||
final i0.Value<String?> thumbnailId;
|
||||
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.thumbnailId = 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(),
|
||||
this.thumbnailId = 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);
|
||||
@ -562,15 +432,15 @@ class LocalAlbumEntityCompanion
|
||||
i0.Expression<String>? id,
|
||||
i0.Expression<String>? name,
|
||||
i0.Expression<DateTime>? updatedAt,
|
||||
i0.Expression<String>? thumbnailId,
|
||||
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 (thumbnailId != null) 'thumbnail_id': thumbnailId,
|
||||
if (backupSelection != null) 'backup_selection': backupSelection,
|
||||
if (marker_ != null) 'marker': marker_,
|
||||
});
|
||||
}
|
||||
|
||||
@ -578,14 +448,14 @@ class LocalAlbumEntityCompanion
|
||||
{i0.Value<String>? id,
|
||||
i0.Value<String>? name,
|
||||
i0.Value<DateTime>? updatedAt,
|
||||
i0.Value<String?>? thumbnailId,
|
||||
i0.Value<i2.BackupSelection>? backupSelection}) {
|
||||
i0.Value<i2.BackupSelection>? backupSelection,
|
||||
i0.Value<bool>? marker_}) {
|
||||
return i1.LocalAlbumEntityCompanion(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
thumbnailId: thumbnailId ?? this.thumbnailId,
|
||||
backupSelection: backupSelection ?? this.backupSelection,
|
||||
marker_: marker_ ?? this.marker_,
|
||||
);
|
||||
}
|
||||
|
||||
@ -601,14 +471,14 @@ class LocalAlbumEntityCompanion
|
||||
if (updatedAt.present) {
|
||||
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
|
||||
}
|
||||
if (thumbnailId.present) {
|
||||
map['thumbnail_id'] = i0.Variable<String>(thumbnailId.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;
|
||||
}
|
||||
|
||||
@ -618,8 +488,8 @@ class LocalAlbumEntityCompanion
|
||||
..write('id: $id, ')
|
||||
..write('name: $name, ')
|
||||
..write('updatedAt: $updatedAt, ')
|
||||
..write('thumbnailId: $thumbnailId, ')
|
||||
..write('backupSelection: $backupSelection')
|
||||
..write('backupSelection: $backupSelection, ')
|
||||
..write('marker_: $marker_')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
@ -41,12 +41,10 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
|
||||
|
||||
@override
|
||||
Future<List<LocalAlbum>> getAll() {
|
||||
final filter = AdvancedCustomFilter(
|
||||
orderBy: [OrderByItem.asc(CustomColumns.base.id)],
|
||||
);
|
||||
|
||||
return PhotoManager.getAssetPathList(hasAll: true, filterOption: filter)
|
||||
.then((e) {
|
||||
return PhotoManager.getAssetPathList(
|
||||
hasAll: true,
|
||||
filterOption: AdvancedCustomFilter(),
|
||||
).then((e) {
|
||||
if (_platform.isAndroid) {
|
||||
e.removeWhere((a) => a.isAll);
|
||||
}
|
||||
|
@ -7,9 +7,9 @@ import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift
|
||||
as i2;
|
||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
|
||||
as i3;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
|
||||
as i4;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
|
||||
as i4;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
|
||||
as i5;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
|
||||
as i6;
|
||||
@ -26,10 +26,10 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
i2.$UserMetadataEntityTable(this);
|
||||
late final i3.$PartnerEntityTable partnerEntity =
|
||||
i3.$PartnerEntityTable(this);
|
||||
late final i4.$LocalAssetEntityTable localAssetEntity =
|
||||
i4.$LocalAssetEntityTable(this);
|
||||
late final i5.$LocalAlbumEntityTable localAlbumEntity =
|
||||
i5.$LocalAlbumEntityTable(this);
|
||||
late final i4.$LocalAlbumEntityTable localAlbumEntity =
|
||||
i4.$LocalAlbumEntityTable(this);
|
||||
late final i5.$LocalAssetEntityTable localAssetEntity =
|
||||
i5.$LocalAssetEntityTable(this);
|
||||
late final i6.$LocalAlbumAssetEntityTable localAlbumAssetEntity =
|
||||
i6.$LocalAlbumAssetEntityTable(this);
|
||||
late final i7.$RemoteAssetEntityTable remoteAssetEntity =
|
||||
@ -43,12 +43,12 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
userEntity,
|
||||
userMetadataEntity,
|
||||
partnerEntity,
|
||||
localAssetEntity,
|
||||
localAlbumEntity,
|
||||
localAssetEntity,
|
||||
localAlbumAssetEntity,
|
||||
remoteAssetEntity,
|
||||
exifEntity,
|
||||
i4.localAssetChecksum,
|
||||
i5.localAssetChecksum,
|
||||
i7.remoteAssetChecksum
|
||||
];
|
||||
@override
|
||||
@ -77,13 +77,6 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
i0.TableUpdate('partner_entity', kind: i0.UpdateKind.delete),
|
||||
],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName('local_asset_entity',
|
||||
limitUpdateKind: i0.UpdateKind.delete),
|
||||
result: [
|
||||
i0.TableUpdate('local_album_entity', kind: i0.UpdateKind.update),
|
||||
],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName('local_asset_entity',
|
||||
limitUpdateKind: i0.UpdateKind.delete),
|
||||
@ -130,10 +123,10 @@ class $DriftManager {
|
||||
i2.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
|
||||
i3.$$PartnerEntityTableTableManager get partnerEntity =>
|
||||
i3.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
|
||||
i4.$$LocalAssetEntityTableTableManager get localAssetEntity =>
|
||||
i4.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity);
|
||||
i5.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
|
||||
i5.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
|
||||
i4.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
|
||||
i4.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
|
||||
i5.$$LocalAssetEntityTableTableManager get localAssetEntity =>
|
||||
i5.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity);
|
||||
i6.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i6
|
||||
.$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity);
|
||||
i7.$$RemoteAssetEntityTableTableManager get remoteAssetEntity =>
|
||||
|
@ -8,6 +8,7 @@ import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.d
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/platform/messages.g.dart' as platform;
|
||||
import 'package:platform/platform.dart';
|
||||
|
||||
class DriftLocalAlbumRepository extends DriftDatabaseRepository
|
||||
@ -65,7 +66,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
|
||||
transaction(() async {
|
||||
await _upsertAssets(assets);
|
||||
// Needs to be after asset upsert to link the thumbnail
|
||||
await _upsertAlbum(localAlbum);
|
||||
await update(localAlbum);
|
||||
await _linkAssetsToAlbum(localAlbum.id, assets);
|
||||
});
|
||||
|
||||
@ -112,7 +113,46 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> update(LocalAlbum localAlbum) => _upsertAlbum(localAlbum);
|
||||
Future<void> update(LocalAlbum localAlbum) {
|
||||
final companion = LocalAlbumEntityCompanion.insert(
|
||||
id: localAlbum.id,
|
||||
name: localAlbum.name,
|
||||
updatedAt: Value(localAlbum.updatedAt),
|
||||
backupSelection: localAlbum.backupSelection,
|
||||
);
|
||||
|
||||
return _db.localAlbumEntity
|
||||
.insertOne(companion, onConflict: DoUpdate((_) => companion));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateAll(Iterable<LocalAlbum> albums) {
|
||||
return _db.transaction(() async {
|
||||
await _db.localAlbumEntity
|
||||
.update()
|
||||
.write(const LocalAlbumEntityCompanion(marker_: Value(false)));
|
||||
|
||||
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(true),
|
||||
);
|
||||
|
||||
batch.insert(
|
||||
_db.localAlbumEntity,
|
||||
companion,
|
||||
onConflict: DoUpdate((_) => companion),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await _db.localAlbumEntity.deleteWhere((f) => f.marker_.equals(false));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<LocalAsset>> getAssetsForAlbum(String albumId) {
|
||||
@ -132,17 +172,30 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<void> _upsertAlbum(LocalAlbum localAlbum) {
|
||||
final companion = LocalAlbumEntityCompanion.insert(
|
||||
id: localAlbum.id,
|
||||
name: localAlbum.name,
|
||||
updatedAt: Value(localAlbum.updatedAt),
|
||||
thumbnailId: Value.absentIfNull(localAlbum.thumbnailId),
|
||||
backupSelection: localAlbum.backupSelection,
|
||||
);
|
||||
@override
|
||||
Future<void> handleSyncDelta(platform.SyncDelta delta) {
|
||||
return _db.transaction(() async {
|
||||
await _deleteAssets(delta.deletes);
|
||||
|
||||
return _db.localAlbumEntity
|
||||
.insertOne(companion, onConflict: DoUpdate((_) => companion));
|
||||
await _upsertAssets(delta.updates.map((a) => a.toLocalAsset()));
|
||||
await _db.batch((batch) {
|
||||
batch.deleteWhere(
|
||||
_db.localAlbumAssetEntity,
|
||||
(f) => f.assetId.isIn(delta.updates.map((a) => a.id)),
|
||||
);
|
||||
for (final asset in delta.updates) {
|
||||
batch.insertAll(
|
||||
_db.localAlbumAssetEntity,
|
||||
asset.albumIds.map(
|
||||
(albumId) => LocalAlbumAssetEntityCompanion.insert(
|
||||
assetId: asset.id,
|
||||
albumId: albumId,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _linkAssetsToAlbum(
|
||||
@ -240,3 +293,18 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on platform.Asset {
|
||||
LocalAsset toLocalAsset() {
|
||||
return LocalAsset(
|
||||
id: id,
|
||||
name: name,
|
||||
type: AssetType.values.elementAtOrNull(type) ?? AssetType.other,
|
||||
createdAt:
|
||||
createdAt == null ? DateTime.now() : DateTime.parse(createdAt!),
|
||||
updatedAt:
|
||||
updatedAt == null ? DateTime.now() : DateTime.parse(updatedAt!),
|
||||
durationInSeconds: durationInSeconds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -9,8 +9,8 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository
|
||||
const DriftLocalAssetRepository(this._db) : super(_db);
|
||||
|
||||
@override
|
||||
Future<LocalAsset> get(String assetId) => _db.managers.localAssetEntity
|
||||
.filter((f) => f.localId(assetId))
|
||||
Future<LocalAsset> get(String id) => _db.managers.localAssetEntity
|
||||
.filter((f) => f.localId(id))
|
||||
.map((a) => a.toDto())
|
||||
.getSingle();
|
||||
}
|
||||
|
55
mobile/lib/platform/messages.dart
Normal file
55
mobile/lib/platform/messages.dart
Normal file
@ -0,0 +1,55 @@
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:pigeon/pigeon.dart';
|
||||
|
||||
@ConfigurePigeon(
|
||||
PigeonOptions(
|
||||
dartOut: 'lib/platform/messages.g.dart',
|
||||
swiftOut: 'ios/Runner/Platform/Messages.g.swift',
|
||||
swiftOptions: SwiftOptions(),
|
||||
kotlinOut:
|
||||
'android/app/src/main/kotlin/app/alextran/immich/platform/messages.g.kt',
|
||||
kotlinOptions: KotlinOptions(),
|
||||
dartOptions: DartOptions(),
|
||||
),
|
||||
)
|
||||
class Asset {
|
||||
final String id;
|
||||
final String name;
|
||||
final int type; // follows AssetType enum from base_asset.model.dart
|
||||
final String? createdAt;
|
||||
final String? updatedAt;
|
||||
final int durationInSeconds;
|
||||
final List<String> albumIds;
|
||||
|
||||
const Asset({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.type,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.durationInSeconds,
|
||||
required this.albumIds,
|
||||
});
|
||||
}
|
||||
|
||||
class SyncDelta {
|
||||
SyncDelta({this.updates = const [], this.deletes = const []});
|
||||
List<Asset> updates;
|
||||
List<String> deletes;
|
||||
}
|
||||
|
||||
@HostApi()
|
||||
abstract class ImHostService {
|
||||
@async
|
||||
bool shouldFullSync();
|
||||
|
||||
@async
|
||||
bool hasMediaChanges();
|
||||
|
||||
@async
|
||||
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
|
||||
SyncDelta getMediaChanges();
|
||||
|
||||
@async
|
||||
void checkpointSync();
|
||||
}
|
310
mobile/lib/platform/messages.g.dart
generated
Normal file
310
mobile/lib/platform/messages.g.dart
generated
Normal file
@ -0,0 +1,310 @@
|
||||
// Autogenerated from Pigeon (v25.3.1), 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 Asset {
|
||||
Asset({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.type,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
required this.durationInSeconds,
|
||||
required this.albumIds,
|
||||
});
|
||||
|
||||
String id;
|
||||
|
||||
String name;
|
||||
|
||||
int type;
|
||||
|
||||
String? createdAt;
|
||||
|
||||
String? updatedAt;
|
||||
|
||||
int durationInSeconds;
|
||||
|
||||
List<String> albumIds;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
durationInSeconds,
|
||||
albumIds,
|
||||
];
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList();
|
||||
}
|
||||
|
||||
static Asset decode(Object result) {
|
||||
result as List<Object?>;
|
||||
return Asset(
|
||||
id: result[0]! as String,
|
||||
name: result[1]! as String,
|
||||
type: result[2]! as int,
|
||||
createdAt: result[3] as String?,
|
||||
updatedAt: result[4] as String?,
|
||||
durationInSeconds: result[5]! as int,
|
||||
albumIds: (result[6] as List<Object?>?)!.cast<String>(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
bool operator ==(Object other) {
|
||||
if (other is! Asset || 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({
|
||||
this.updates = const [],
|
||||
this.deletes = const [],
|
||||
});
|
||||
|
||||
List<Asset> updates;
|
||||
|
||||
List<String> deletes;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[
|
||||
updates,
|
||||
deletes,
|
||||
];
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList();
|
||||
}
|
||||
|
||||
static SyncDelta decode(Object result) {
|
||||
result as List<Object?>;
|
||||
return SyncDelta(
|
||||
updates: (result[0] as List<Object?>?)!.cast<Asset>(),
|
||||
deletes: (result[1] as List<Object?>?)!.cast<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 Asset) {
|
||||
buffer.putUint8(129);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is SyncDelta) {
|
||||
buffer.putUint8(130);
|
||||
writeValue(buffer, value.encode());
|
||||
} else {
|
||||
super.writeValue(buffer, value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Object? readValueOfType(int type, ReadBuffer buffer) {
|
||||
switch (type) {
|
||||
case 129:
|
||||
return Asset.decode(readValue(buffer)!);
|
||||
case 130:
|
||||
return SyncDelta.decode(readValue(buffer)!);
|
||||
default:
|
||||
return super.readValueOfType(type, buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ImHostService {
|
||||
/// Constructor for [ImHostService]. The [binaryMessenger] named argument is
|
||||
/// available for dependency injection. If it is left null, the default
|
||||
/// BinaryMessenger will be used which routes to the host platform.
|
||||
ImHostService(
|
||||
{BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
||||
: 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.ImHostService.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<bool> hasMediaChanges() async {
|
||||
final String pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.ImHostService.hasMediaChanges$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.ImHostService.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.ImHostService.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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/platform/messages.g.dart';
|
||||
|
||||
final platformMessagesImpl = Provider<ImHostService>((_) => ImHostService());
|
@ -5,15 +5,15 @@ import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.da
|
||||
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
|
||||
final deviceSyncServiceProvider = Provider(
|
||||
(ref) => DeviceSyncService(
|
||||
albumMediaRepository: ref.watch(albumMediaRepositoryProvider),
|
||||
localAlbumRepository: ref.watch(localAlbumRepository),
|
||||
localAssetRepository: ref.watch(localAssetProvider),
|
||||
hostService: ref.watch(platformMessagesImpl),
|
||||
),
|
||||
);
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -5,31 +5,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
|
||||
sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "76.0.0"
|
||||
_macros:
|
||||
dependency: transitive
|
||||
description: dart
|
||||
source: sdk
|
||||
version: "0.3.3"
|
||||
version: "80.0.0"
|
||||
analyzer:
|
||||
dependency: "direct overridden"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
|
||||
sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.11.0"
|
||||
version: "7.3.0"
|
||||
analyzer_plugin:
|
||||
dependency: "direct overridden"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer_plugin
|
||||
sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161"
|
||||
sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.11.3"
|
||||
version: "0.13.0"
|
||||
ansicolor:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -74,10 +69,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: auto_route_generator
|
||||
sha256: c9086eb07271e51b44071ad5cff34e889f3156710b964a308c2ab590769e79e6
|
||||
sha256: c2e359d8932986d4d1bcad7a428143f81384ce10fef8d4aa5bc29e1f83766a46
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.0.0"
|
||||
version: "9.3.1"
|
||||
background_downloader:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -322,34 +317,42 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: custom_lint
|
||||
sha256: "4500e88854e7581ee43586abeaf4443cb22375d6d289241a87b1aadf678d5545"
|
||||
sha256: "409c485fd14f544af1da965d5a0d160ee57cd58b63eeaa7280a4f28cf5bda7f1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.10"
|
||||
version: "0.7.5"
|
||||
custom_lint_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: custom_lint_builder
|
||||
sha256: "5a95eff100da256fbf086b329c17c8b49058c261cdf56d3a4157d3c31c511d78"
|
||||
sha256: "107e0a43606138015777590ee8ce32f26ba7415c25b722ff0908a6f5d7a4c228"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.10"
|
||||
version: "0.7.5"
|
||||
custom_lint_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: custom_lint_core
|
||||
sha256: "76a4046cc71d976222a078a8fd4a65e198b70545a8d690a75196dd14f08510f6"
|
||||
sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.10"
|
||||
version: "0.7.5"
|
||||
custom_lint_visitor:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: custom_lint_visitor
|
||||
sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0+7.3.0"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820"
|
||||
sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.8"
|
||||
version: "3.0.1"
|
||||
dartx:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -675,10 +678,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: freezed_annotation
|
||||
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
|
||||
sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
version: "3.0.0"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -923,10 +926,11 @@ packages:
|
||||
isar_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: isar_generator
|
||||
sha256: "484e73d3b7e81dbd816852fe0b9497333118a9aeb646fd2d349a62cc8980ffe1"
|
||||
url: "https://pub.isar-community.dev"
|
||||
source: hosted
|
||||
path: "packages/isar_generator"
|
||||
ref: v3
|
||||
resolved-ref: ad574f60ed6f39d2995cd16fc7dc3de9a646ef30
|
||||
url: "https://github.com/callumw-k/isar"
|
||||
source: git
|
||||
version: "3.1.8"
|
||||
js:
|
||||
dependency: transitive
|
||||
@ -984,14 +988,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
macros:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: macros
|
||||
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3-main.0"
|
||||
maplibre_gl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1033,7 +1029,7 @@ packages:
|
||||
source: hosted
|
||||
version: "0.11.1"
|
||||
meta:
|
||||
dependency: "direct overridden"
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
@ -1264,6 +1260,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
pigeon:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: pigeon
|
||||
sha256: "3e4e6258f22760fa11f86d2a5202fb3f8367cb361d33bd9a93de85a7959e9976"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "25.3.1"
|
||||
platform:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1348,10 +1352,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: riverpod_analyzer_utils
|
||||
sha256: "0dcb0af32d561f8fa000c6a6d95633c9fb08ea8a8df46e3f9daca59f11218167"
|
||||
sha256: "03a17170088c63aab6c54c44456f5ab78876a1ddb6032ffde1662ddab4959611"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.6"
|
||||
version: "0.5.10"
|
||||
riverpod_annotation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1364,18 +1368,18 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: riverpod_generator
|
||||
sha256: "851aedac7ad52693d12af3bf6d92b1626d516ed6b764eb61bf19e968b5e0b931"
|
||||
sha256: "44a0992d54473eb199ede00e2260bd3c262a86560e3c6f6374503d86d0580e36"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
version: "2.6.5"
|
||||
riverpod_lint:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: riverpod_lint
|
||||
sha256: "0684c21a9a4582c28c897d55c7b611fa59a351579061b43f8c92c005804e63a8"
|
||||
sha256: "89a52b7334210dbff8605c3edf26cfe69b15062beed5cbfeff2c3812c33c9e35"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
version: "2.6.5"
|
||||
rxdart:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1537,10 +1541,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_gen
|
||||
sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
|
||||
sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
version: "2.0.0"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -82,11 +82,6 @@ dependencies:
|
||||
drift: ^2.23.1
|
||||
drift_flutter: ^0.2.4
|
||||
|
||||
dependency_overrides:
|
||||
analyzer: ^6.0.0
|
||||
meta: ^1.11.0
|
||||
analyzer_plugin: ^0.11.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
@ -96,11 +91,13 @@ dev_dependencies:
|
||||
flutter_launcher_icons: ^0.14.3
|
||||
flutter_native_splash: ^2.4.5
|
||||
isar_generator:
|
||||
version: *isar_version
|
||||
hosted: https://pub.isar-community.dev/
|
||||
git:
|
||||
url: https://github.com/callumw-k/isar
|
||||
ref: v3
|
||||
path: packages/isar_generator/
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
custom_lint: ^0.6.4
|
||||
custom_lint: ^0.7.5
|
||||
riverpod_lint: ^2.6.1
|
||||
riverpod_generator: ^2.6.1
|
||||
mocktail: ^1.0.4
|
||||
@ -110,6 +107,8 @@ dev_dependencies:
|
||||
file: ^7.0.1 # for MemoryFileSystem
|
||||
# Drift generator
|
||||
drift_dev: ^2.23.1
|
||||
# Type safe platform code
|
||||
pigeon: ^25.3.1
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/domain/services/user.service.dart';
|
||||
import 'package:immich_mobile/domain/utils/background_sync.dart';
|
||||
import 'package:immich_mobile/platform/messages.g.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockStoreService extends Mock implements StoreService {}
|
||||
@ -8,3 +9,5 @@ class MockStoreService extends Mock implements StoreService {}
|
||||
class MockUserService extends Mock implements UserService {}
|
||||
|
||||
class MockBackgroundSyncManager extends Mock implements BackgroundSyncManager {}
|
||||
|
||||
class MockHostService extends Mock implements ImHostService {}
|
||||
|
@ -2,21 +2,21 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/album_media.interface.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/local_album.interface.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/local_asset.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/services/device_sync.service.dart';
|
||||
import 'package:immich_mobile/utils/nullable_value.dart';
|
||||
import 'package:immich_mobile/platform/messages.g.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../fixtures/local_album.stub.dart';
|
||||
import '../../fixtures/local_asset.stub.dart';
|
||||
import '../../infrastructure/repository.mock.dart';
|
||||
import '../service.mock.dart';
|
||||
|
||||
void main() {
|
||||
late IAlbumMediaRepository mockAlbumMediaRepo;
|
||||
late ILocalAlbumRepository mockLocalAlbumRepo;
|
||||
late ILocalAssetRepository mockLocalAssetRepo;
|
||||
late ImHostService mockHostService;
|
||||
late DeviceSyncService sut;
|
||||
|
||||
Future<T> mockTransaction<T>(Future<T> Function() action) => action();
|
||||
@ -24,12 +24,12 @@ void main() {
|
||||
setUp(() {
|
||||
mockAlbumMediaRepo = MockAlbumMediaRepository();
|
||||
mockLocalAlbumRepo = MockLocalAlbumRepository();
|
||||
mockLocalAssetRepo = MockLocalAssetRepository();
|
||||
mockHostService = MockHostService();
|
||||
|
||||
sut = DeviceSyncService(
|
||||
albumMediaRepository: mockAlbumMediaRepo,
|
||||
localAlbumRepository: mockLocalAlbumRepo,
|
||||
localAssetRepository: mockLocalAssetRepo,
|
||||
hostService: mockHostService,
|
||||
);
|
||||
|
||||
registerFallbackValue(LocalAlbumStub.album1);
|
||||
@ -51,8 +51,6 @@ void main() {
|
||||
.thenAnswer((_) async => true);
|
||||
when(() => mockLocalAlbumRepo.getAssetsForAlbum(any()))
|
||||
.thenAnswer((_) async => []);
|
||||
when(() => mockLocalAssetRepo.get(any()))
|
||||
.thenAnswer((_) async => LocalAssetStub.image1);
|
||||
when(() => mockAlbumMediaRepo.refresh(any())).thenAnswer(
|
||||
(inv) async =>
|
||||
LocalAlbumStub.album1.copyWith(id: inv.positionalArguments.first),
|
||||
@ -250,7 +248,7 @@ void main() {
|
||||
|
||||
group('addAlbum', () {
|
||||
test(
|
||||
'refreshes, gets assets, sets thumbnail, and inserts for non-empty album',
|
||||
'refreshes, gets assets, and inserts for non-empty album',
|
||||
() async {
|
||||
final newAlbum = LocalAlbumStub.album1.copyWith(assetCount: 0);
|
||||
final refreshedAlbum =
|
||||
@ -284,38 +282,33 @@ void main() {
|
||||
expect(capturedAlbum.id, newAlbum.id);
|
||||
expect(capturedAlbum.assetCount, refreshedAlbum.assetCount);
|
||||
expect(capturedAlbum.updatedAt, refreshedAlbum.updatedAt);
|
||||
expect(capturedAlbum.thumbnailId, assets.first.id);
|
||||
expect(listEquals(capturedAssets, assets), isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'refreshes, skips assets, sets null thumbnail, and inserts for empty album',
|
||||
() async {
|
||||
final newAlbum = LocalAlbumStub.album1.copyWith(assetCount: 0);
|
||||
final refreshedAlbum =
|
||||
newAlbum.copyWith(updatedAt: DateTime(2024), assetCount: 0);
|
||||
test('refreshes, skips assets, and inserts for empty album', () async {
|
||||
final newAlbum = LocalAlbumStub.album1.copyWith(assetCount: 0);
|
||||
final refreshedAlbum =
|
||||
newAlbum.copyWith(updatedAt: DateTime(2024), assetCount: 0);
|
||||
|
||||
when(() => mockAlbumMediaRepo.refresh(newAlbum.id))
|
||||
.thenAnswer((_) async => refreshedAlbum);
|
||||
when(() => mockAlbumMediaRepo.refresh(newAlbum.id))
|
||||
.thenAnswer((_) async => refreshedAlbum);
|
||||
|
||||
await sut.addAlbum(newAlbum);
|
||||
await sut.addAlbum(newAlbum);
|
||||
|
||||
verify(() => mockAlbumMediaRepo.refresh(newAlbum.id)).called(1);
|
||||
verifyNever(() => mockAlbumMediaRepo.getAssetsForAlbum(newAlbum.id));
|
||||
verify(() => mockAlbumMediaRepo.refresh(newAlbum.id)).called(1);
|
||||
verifyNever(() => mockAlbumMediaRepo.getAssetsForAlbum(newAlbum.id));
|
||||
|
||||
final captured =
|
||||
verify(() => mockLocalAlbumRepo.insert(captureAny(), captureAny()))
|
||||
.captured;
|
||||
final capturedAlbum = captured.first as LocalAlbum;
|
||||
final capturedAssets = captured[1] as List<Object?>;
|
||||
final captured =
|
||||
verify(() => mockLocalAlbumRepo.insert(captureAny(), captureAny()))
|
||||
.captured;
|
||||
final capturedAlbum = captured.first as LocalAlbum;
|
||||
final capturedAssets = captured[1] as List<Object?>;
|
||||
|
||||
expect(capturedAlbum.id, newAlbum.id);
|
||||
expect(capturedAlbum.assetCount, 0);
|
||||
expect(capturedAlbum.thumbnailId, isNull);
|
||||
expect(capturedAssets, isEmpty);
|
||||
},
|
||||
);
|
||||
expect(capturedAlbum.id, newAlbum.id);
|
||||
expect(capturedAlbum.assetCount, 0);
|
||||
expect(capturedAssets, isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('removeAlbum', () {
|
||||
@ -361,11 +354,7 @@ void main() {
|
||||
updateTimeCond: any(named: 'updateTimeCond'),
|
||||
),
|
||||
).thenAnswer((_) async => [newAsset]);
|
||||
final dbAlbumNoThumb =
|
||||
dbAlbum.copyWith(thumbnailId: const NullableValue.absent());
|
||||
|
||||
final result =
|
||||
await sut.updateAlbum(dbAlbumNoThumb, LocalAlbumStub.album1);
|
||||
final result = await sut.updateAlbum(dbAlbum, LocalAlbumStub.album1);
|
||||
|
||||
expect(result, isTrue);
|
||||
verify(() => mockAlbumMediaRepo.refresh(dbAlbum.id)).called(1);
|
||||
@ -376,7 +365,6 @@ void main() {
|
||||
updateTimeCond: any(named: 'updateTimeCond'),
|
||||
),
|
||||
).called(1);
|
||||
verifyNever(() => mockLocalAssetRepo.get(any()));
|
||||
|
||||
verify(() => mockLocalAlbumRepo.transaction<void>(any())).called(1);
|
||||
verify(() => mockLocalAlbumRepo.addAssets(dbAlbum.id, [newAsset]))
|
||||
@ -385,10 +373,7 @@ void main() {
|
||||
() => mockLocalAlbumRepo.update(
|
||||
any(
|
||||
that: predicate<LocalAlbum>(
|
||||
(a) =>
|
||||
a.id == dbAlbum.id &&
|
||||
a.assetCount == 2 &&
|
||||
a.thumbnailId == newAsset.id,
|
||||
(a) => a.id == dbAlbum.id && a.assetCount == 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -437,10 +422,7 @@ void main() {
|
||||
() => mockLocalAlbumRepo.update(
|
||||
any(
|
||||
that: predicate<LocalAlbum>(
|
||||
(a) =>
|
||||
a.id == dbAlbum.id &&
|
||||
a.assetCount == 0 &&
|
||||
a.thumbnailId == null,
|
||||
(a) => a.id == dbAlbum.id && a.assetCount == 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -508,11 +490,8 @@ void main() {
|
||||
});
|
||||
|
||||
group('checkAddition', () {
|
||||
final dbAlbum = LocalAlbumStub.album1.copyWith(
|
||||
updatedAt: DateTime(2024, 1, 1, 10, 0, 0),
|
||||
assetCount: 1,
|
||||
thumbnailId: const NullableValue.value("thumb1"),
|
||||
);
|
||||
final dbAlbum = LocalAlbumStub.album1
|
||||
.copyWith(updatedAt: DateTime(2024, 1, 1, 10, 0, 0), assetCount: 1);
|
||||
final refreshedAlbum = dbAlbum.copyWith(
|
||||
updatedAt: DateTime(2024, 1, 1, 11, 0, 0),
|
||||
assetCount: 2,
|
||||
@ -530,13 +509,6 @@ void main() {
|
||||
),
|
||||
).thenAnswer((_) async => [newAsset]);
|
||||
|
||||
when(() => mockLocalAssetRepo.get("thumb1")).thenAnswer(
|
||||
(_) async => LocalAssetStub.image1.copyWith(
|
||||
id: "thumb1",
|
||||
createdAt: DateTime(2024, 1, 1, 9, 0, 0),
|
||||
),
|
||||
);
|
||||
|
||||
final result = await sut.checkAddition(dbAlbum, refreshedAlbum);
|
||||
|
||||
expect(result, isTrue);
|
||||
@ -546,19 +518,15 @@ void main() {
|
||||
updateTimeCond: any(named: 'updateTimeCond'),
|
||||
),
|
||||
).called(1);
|
||||
verify(() => mockLocalAssetRepo.get("thumb1")).called(1);
|
||||
verify(() => mockLocalAlbumRepo.addAssets(dbAlbum.id, [newAsset]))
|
||||
.called(1);
|
||||
verify(
|
||||
() => mockLocalAlbumRepo.update(
|
||||
any(
|
||||
that: predicate<LocalAlbum>(
|
||||
(a) =>
|
||||
a.id == dbAlbum.id &&
|
||||
a.assetCount == 2 &&
|
||||
a.updatedAt == refreshedAlbum.updatedAt &&
|
||||
a.thumbnailId == newAsset.id,
|
||||
),
|
||||
that: predicate<LocalAlbum>((a) =>
|
||||
a.id == dbAlbum.id &&
|
||||
a.assetCount == 2 &&
|
||||
a.updatedAt == refreshedAlbum.updatedAt),
|
||||
),
|
||||
),
|
||||
).called(1);
|
||||
@ -566,93 +534,6 @@ void main() {
|
||||
.called(1);
|
||||
});
|
||||
|
||||
test('returns true and keeps old thumbnail if newer', () async {
|
||||
final newAsset = LocalAssetStub.image2.copyWith(
|
||||
id: "asset2",
|
||||
createdAt: DateTime(2024, 1, 1, 8, 0, 0),
|
||||
);
|
||||
when(
|
||||
() => mockAlbumMediaRepo.getAssetsForAlbum(
|
||||
dbAlbum.id,
|
||||
updateTimeCond: any(named: 'updateTimeCond'),
|
||||
),
|
||||
).thenAnswer((_) async => [newAsset]);
|
||||
|
||||
when(() => mockLocalAssetRepo.get("thumb1")).thenAnswer(
|
||||
(_) async => LocalAssetStub.image1.copyWith(
|
||||
id: "thumb1",
|
||||
createdAt: DateTime(2024, 1, 1, 9, 0, 0),
|
||||
),
|
||||
);
|
||||
|
||||
final result = await sut.checkAddition(dbAlbum, refreshedAlbum);
|
||||
|
||||
expect(result, isTrue);
|
||||
verify(
|
||||
() => mockAlbumMediaRepo.getAssetsForAlbum(
|
||||
dbAlbum.id,
|
||||
updateTimeCond: any(named: 'updateTimeCond'),
|
||||
),
|
||||
).called(1);
|
||||
verify(() => mockLocalAssetRepo.get("thumb1")).called(1);
|
||||
verify(() => mockLocalAlbumRepo.addAssets(dbAlbum.id, [newAsset]))
|
||||
.called(1);
|
||||
verify(
|
||||
() => mockLocalAlbumRepo.update(
|
||||
any(
|
||||
that: predicate<LocalAlbum>(
|
||||
(a) =>
|
||||
a.id == dbAlbum.id &&
|
||||
a.assetCount == 2 &&
|
||||
a.updatedAt == refreshedAlbum.updatedAt &&
|
||||
a.thumbnailId == "thumb1",
|
||||
),
|
||||
),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('returns true and sets new thumbnail if db thumb is null', () async {
|
||||
final dbAlbumNoThumb =
|
||||
dbAlbum.copyWith(thumbnailId: const NullableValue.empty());
|
||||
final newAsset = LocalAssetStub.image2.copyWith(
|
||||
id: "asset2",
|
||||
createdAt: DateTime(2024, 1, 1, 10, 30, 0),
|
||||
);
|
||||
when(
|
||||
() => mockAlbumMediaRepo.getAssetsForAlbum(
|
||||
dbAlbum.id,
|
||||
updateTimeCond: any(named: 'updateTimeCond'),
|
||||
),
|
||||
).thenAnswer((_) async => [newAsset]);
|
||||
|
||||
final result = await sut.checkAddition(dbAlbumNoThumb, refreshedAlbum);
|
||||
|
||||
expect(result, isTrue);
|
||||
verify(
|
||||
() => mockAlbumMediaRepo.getAssetsForAlbum(
|
||||
dbAlbum.id,
|
||||
updateTimeCond: any(named: 'updateTimeCond'),
|
||||
),
|
||||
).called(1);
|
||||
verifyNever(() => mockLocalAssetRepo.get(any()));
|
||||
verify(() => mockLocalAlbumRepo.addAssets(dbAlbum.id, [newAsset]))
|
||||
.called(1);
|
||||
verify(
|
||||
() => mockLocalAlbumRepo.update(
|
||||
any(
|
||||
that: predicate<LocalAlbum>(
|
||||
(a) =>
|
||||
a.id == dbAlbum.id &&
|
||||
a.assetCount == 2 &&
|
||||
a.updatedAt == refreshedAlbum.updatedAt &&
|
||||
a.thumbnailId == newAsset.id,
|
||||
),
|
||||
),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('returns false if assetCount decreased', () async {
|
||||
final decreasedCountAlbum = refreshedAlbum.copyWith(assetCount: 0);
|
||||
final result = await sut.checkAddition(dbAlbum, decreasedCountAlbum);
|
||||
@ -725,7 +606,6 @@ void main() {
|
||||
final dbAlbum = LocalAlbumStub.album1.copyWith(
|
||||
updatedAt: DateTime(2024, 1, 1),
|
||||
assetCount: 2,
|
||||
thumbnailId: const NullableValue.value("asset1"),
|
||||
);
|
||||
final refreshedAlbum = dbAlbum.copyWith(
|
||||
updatedAt: DateTime(2024, 1, 2),
|
||||
@ -760,7 +640,7 @@ void main() {
|
||||
when(() => mockLocalAlbumRepo.getAssetsForAlbum(dbAlbum.id))
|
||||
.thenAnswer((_) async => [dbAsset1, dbAsset2]);
|
||||
|
||||
final result = await sut.fullSync(dbAlbum, emptyRefreshedAlbum);
|
||||
final result = await sut.fullDiff(dbAlbum, emptyRefreshedAlbum);
|
||||
|
||||
expect(result, isTrue);
|
||||
verifyNever(
|
||||
@ -773,13 +653,10 @@ void main() {
|
||||
verify(
|
||||
() => mockLocalAlbumRepo.update(
|
||||
any(
|
||||
that: predicate<LocalAlbum>(
|
||||
(a) =>
|
||||
a.id == dbAlbum.id &&
|
||||
a.assetCount == 0 &&
|
||||
a.updatedAt == emptyRefreshedAlbum.updatedAt &&
|
||||
a.thumbnailId == null,
|
||||
),
|
||||
that: predicate<LocalAlbum>((a) =>
|
||||
a.id == dbAlbum.id &&
|
||||
a.assetCount == 0 &&
|
||||
a.updatedAt == emptyRefreshedAlbum.updatedAt),
|
||||
),
|
||||
),
|
||||
).called(1);
|
||||
@ -789,10 +666,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('handles empty DB album -> adds all device assets', () async {
|
||||
final emptyDbAlbum = dbAlbum.copyWith(
|
||||
assetCount: 0,
|
||||
thumbnailId: const NullableValue.empty(),
|
||||
);
|
||||
final emptyDbAlbum = dbAlbum.copyWith(assetCount: 0);
|
||||
final deviceAssets = [deviceAsset1, deviceAsset3];
|
||||
|
||||
deviceAssets.sort((a, b) => a.createdAt.compareTo(b.createdAt));
|
||||
@ -804,7 +678,7 @@ void main() {
|
||||
when(() => mockLocalAlbumRepo.getAssetsForAlbum(emptyDbAlbum.id))
|
||||
.thenAnswer((_) async => []);
|
||||
|
||||
final result = await sut.fullSync(emptyDbAlbum, refreshedWithAssets);
|
||||
final result = await sut.fullDiff(emptyDbAlbum, refreshedWithAssets);
|
||||
|
||||
expect(result, isTrue);
|
||||
verify(() => mockAlbumMediaRepo.getAssetsForAlbum(emptyDbAlbum.id))
|
||||
@ -816,13 +690,10 @@ void main() {
|
||||
verify(
|
||||
() => mockLocalAlbumRepo.update(
|
||||
any(
|
||||
that: predicate<LocalAlbum>(
|
||||
(a) =>
|
||||
a.id == emptyDbAlbum.id &&
|
||||
a.assetCount == deviceAssets.length &&
|
||||
a.updatedAt == refreshedWithAssets.updatedAt &&
|
||||
a.thumbnailId == deviceAssets.first.id,
|
||||
),
|
||||
that: predicate<LocalAlbum>((a) =>
|
||||
a.id == emptyDbAlbum.id &&
|
||||
a.assetCount == deviceAssets.length &&
|
||||
a.updatedAt == refreshedWithAssets.updatedAt),
|
||||
),
|
||||
),
|
||||
).called(1);
|
||||
@ -844,7 +715,7 @@ void main() {
|
||||
(_) async => dbAssets,
|
||||
);
|
||||
|
||||
final result = await sut.fullSync(dbAlbum, currentRefreshedAlbum);
|
||||
final result = await sut.fullDiff(dbAlbum, currentRefreshedAlbum);
|
||||
|
||||
expect(result, isTrue);
|
||||
verify(() => mockAlbumMediaRepo.getAssetsForAlbum(dbAlbum.id)).called(1);
|
||||
@ -871,13 +742,10 @@ void main() {
|
||||
verify(
|
||||
() => mockLocalAlbumRepo.update(
|
||||
any(
|
||||
that: predicate<LocalAlbum>(
|
||||
(a) =>
|
||||
a.id == dbAlbum.id &&
|
||||
a.assetCount == 2 &&
|
||||
a.updatedAt == currentRefreshedAlbum.updatedAt &&
|
||||
a.thumbnailId == deviceAssets.first.id,
|
||||
),
|
||||
that: predicate<LocalAlbum>((a) =>
|
||||
a.id == dbAlbum.id &&
|
||||
a.assetCount == 2 &&
|
||||
a.updatedAt == currentRefreshedAlbum.updatedAt),
|
||||
),
|
||||
),
|
||||
).called(1);
|
||||
@ -901,7 +769,7 @@ void main() {
|
||||
when(() => mockLocalAlbumRepo.getAssetsForAlbum(dbAlbum.id))
|
||||
.thenAnswer((_) async => dbAssets);
|
||||
|
||||
final result = await sut.fullSync(dbAlbum, currentRefreshedAlbum);
|
||||
final result = await sut.fullDiff(dbAlbum, currentRefreshedAlbum);
|
||||
|
||||
expect(result, isTrue);
|
||||
verify(() => mockAlbumMediaRepo.getAssetsForAlbum(dbAlbum.id)).called(1);
|
||||
@ -912,13 +780,10 @@ void main() {
|
||||
verify(
|
||||
() => mockLocalAlbumRepo.update(
|
||||
any(
|
||||
that: predicate<LocalAlbum>(
|
||||
(a) =>
|
||||
a.id == dbAlbum.id &&
|
||||
a.assetCount == 2 &&
|
||||
a.updatedAt == currentRefreshedAlbum.updatedAt &&
|
||||
a.thumbnailId == deviceAssets.first.id,
|
||||
),
|
||||
that: predicate<LocalAlbum>((a) =>
|
||||
a.id == dbAlbum.id &&
|
||||
a.assetCount == 2 &&
|
||||
a.updatedAt == currentRefreshedAlbum.updatedAt),
|
||||
),
|
||||
),
|
||||
).called(1);
|
||||
|
Loading…
x
Reference in New Issue
Block a user