feat: native sync ios

This commit is contained in:
shenlong-tanwen 2025-05-02 19:52:17 +05:30
parent dbe1a127c9
commit 214893d2f4
27 changed files with 1757 additions and 971 deletions

View File

@ -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)
}
}
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -5,31 +5,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _fe_analyzer_shared name: _fe_analyzer_shared
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "76.0.0" version: "80.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.3"
analyzer: analyzer:
dependency: "direct main" dependency: "direct main"
description: description:
name: analyzer name: analyzer
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.11.0" version: "7.3.0"
analyzer_plugin: analyzer_plugin:
dependency: "direct main" dependency: "direct main"
description: description:
name: analyzer_plugin name: analyzer_plugin
sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.3" version: "0.13.0"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -106,34 +101,42 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: custom_lint name: custom_lint
sha256: "4500e88854e7581ee43586abeaf4443cb22375d6d289241a87b1aadf678d5545" sha256: "409c485fd14f544af1da965d5a0d160ee57cd58b63eeaa7280a4f28cf5bda7f1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.10" version: "0.7.5"
custom_lint_builder: custom_lint_builder:
dependency: "direct main" dependency: "direct main"
description: description:
name: custom_lint_builder name: custom_lint_builder
sha256: "5a95eff100da256fbf086b329c17c8b49058c261cdf56d3a4157d3c31c511d78" sha256: "107e0a43606138015777590ee8ce32f26ba7415c25b722ff0908a6f5d7a4c228"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.10" version: "0.7.5"
custom_lint_core: custom_lint_core:
dependency: transitive dependency: transitive
description: description:
name: custom_lint_core name: custom_lint_core
sha256: "76a4046cc71d976222a078a8fd4a65e198b70545a8d690a75196dd14f08510f6" sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted 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: dart_style:
dependency: transitive dependency: transitive
description: description:
name: dart_style name: dart_style
sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.8" version: "3.0.1"
file: file:
dependency: transitive dependency: transitive
description: description:
@ -154,10 +157,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: freezed_annotation name: freezed_annotation
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.4" version: "3.0.0"
glob: glob:
dependency: "direct main" dependency: "direct main"
description: description:
@ -198,14 +201,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" 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: matcher:
dependency: transitive dependency: transitive
description: description:

View File

@ -5,9 +5,9 @@ environment:
sdk: '>=3.0.0 <4.0.0' sdk: '>=3.0.0 <4.0.0'
dependencies: dependencies:
analyzer: ^6.0.0 analyzer: ^7.0.0
analyzer_plugin: ^0.11.3 analyzer_plugin: ^0.13.0
custom_lint_builder: ^0.6.4 custom_lint_builder: ^0.7.5
glob: ^2.1.2 glob: ^2.1.2
dev_dependencies: dev_dependencies:

View File

@ -89,6 +89,20 @@
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = "<group>"; }; FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
B28F36282DC3150F00B18015 /* Platform */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
explicitFileTypes = {
};
explicitFolders = (
);
path = Platform;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = { 97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
@ -175,6 +189,7 @@
97C146F01CF9000F007C117D /* Runner */ = { 97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
B28F36282DC3150F00B18015 /* Platform */,
FA9973382CF6DF4B000EF859 /* Runner.entitlements */, FA9973382CF6DF4B000EF859 /* Runner.entitlements */,
65DD438629917FAD0047FFA8 /* BackgroundSync */, 65DD438629917FAD0047FFA8 /* BackgroundSync */,
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */, FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */,
@ -224,6 +239,9 @@
dependencies = ( dependencies = (
FAC6F8992D287C890078CB2F /* PBXTargetDependency */, FAC6F8992D287C890078CB2F /* PBXTargetDependency */,
); );
fileSystemSynchronizedGroups = (
B28F36282DC3150F00B18015 /* Platform */,
);
name = Runner; name = Runner;
productName = Runner; productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Immich-Debug.app */; productReference = 97C146EE1CF9000F007C117D /* Immich-Debug.app */;

View File

@ -22,6 +22,10 @@ import UIKit
BackgroundServicePlugin.registerBackgroundProcessing() BackgroundServicePlugin.registerBackgroundProcessing()
BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!) 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 BackgroundServicePlugin.setPluginRegistrantCallback { registry in
if !registry.hasPlugin("org.cocoapods.path-provider-foundation") { if !registry.hasPlugin("org.cocoapods.path-provider-foundation") {

View 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
}
}

View 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)
}
}
}

View 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(()))
}
}
}

View File

@ -1,6 +1,7 @@
import 'package:immich_mobile/domain/interfaces/db.interface.dart'; 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/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/local_album.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 { abstract interface class ILocalAlbumRepository implements IDatabaseRepository {
Future<void> insert(LocalAlbum album, Iterable<LocalAsset> assets); 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> update(LocalAlbum album);
Future<void> updateAll(Iterable<LocalAlbum> albums);
Future<void> handleSyncDelta(SyncDelta delta);
Future<void> delete(String albumId); Future<void> delete(String albumId);
Future<void> removeAssets(String albumId, Iterable<String> assetIds); Future<void> removeAssets(String albumId, Iterable<String> assetIds);

View File

@ -2,5 +2,5 @@ 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/asset/base_asset.model.dart';
abstract interface class ILocalAssetRepository implements IDatabaseRepository { abstract interface class ILocalAssetRepository implements IDatabaseRepository {
Future<LocalAsset> get(String assetId); Future<LocalAsset> get(String id);
} }

View File

@ -4,28 +4,50 @@ import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:immich_mobile/domain/interfaces/album_media.interface.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_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/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/local_album.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/diff.dart';
import 'package:immich_mobile/utils/nullable_value.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
class DeviceSyncService { class DeviceSyncService {
final IAlbumMediaRepository _albumMediaRepository; final IAlbumMediaRepository _albumMediaRepository;
final ILocalAlbumRepository _localAlbumRepository; final ILocalAlbumRepository _localAlbumRepository;
final ILocalAssetRepository _localAssetRepository; final platform.ImHostService _hostService;
final Logger _log = Logger("SyncService"); final Logger _log = Logger("SyncService");
DeviceSyncService({ DeviceSyncService({
required IAlbumMediaRepository albumMediaRepository, required IAlbumMediaRepository albumMediaRepository,
required ILocalAlbumRepository localAlbumRepository, required ILocalAlbumRepository localAlbumRepository,
required ILocalAssetRepository localAssetRepository, required platform.ImHostService hostService,
}) : _albumMediaRepository = albumMediaRepository, }) : _albumMediaRepository = albumMediaRepository,
_localAlbumRepository = localAlbumRepository, _localAlbumRepository = localAlbumRepository,
_localAssetRepository = localAssetRepository; _hostService = hostService;
Future<void> sync() async { 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 { try {
final Stopwatch stopwatch = Stopwatch()..start(); final Stopwatch stopwatch = Stopwatch()..start();
// The deviceAlbums will not have the updatedAt field // The deviceAlbums will not have the updatedAt field
@ -47,6 +69,7 @@ class DeviceSyncService {
onlySecond: addAlbum, onlySecond: addAlbum,
); );
_hostService.checkpointSync();
stopwatch.stop(); stopwatch.stop();
_log.info("Full device sync took - ${stopwatch.elapsedMilliseconds}ms"); _log.info("Full device sync took - ${stopwatch.elapsedMilliseconds}ms");
} catch (e, s) { } catch (e, s) {
@ -57,17 +80,12 @@ class DeviceSyncService {
Future<void> addAlbum(LocalAlbum newAlbum) async { Future<void> addAlbum(LocalAlbum newAlbum) async {
try { try {
_log.info("Adding device album ${newAlbum.name}"); _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 final assets = album.assetCount > 0
? await _albumMediaRepository.getAssetsForAlbum(deviceAlbum.id) ? await _albumMediaRepository.getAssetsForAlbum(album.id)
: <LocalAsset>[]; : <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); await _localAlbumRepository.insert(album, assets);
_log.fine("Successfully added device album ${album.name}"); _log.fine("Successfully added device album ${album.name}");
} catch (e, s) { } catch (e, s) {
@ -110,7 +128,7 @@ class DeviceSyncService {
} }
// Slower path - full sync // Slower path - full sync
return await fullSync(dbAlbum, deviceAlbum); return await fullDiff(dbAlbum, deviceAlbum);
} catch (e, s) { } catch (e, s) {
_log.warning("Error while diff device album", e, s); _log.warning("Error while diff device album", e, s);
} }
@ -155,25 +173,8 @@ class DeviceSyncService {
return false; 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( await _updateAlbum(
deviceAlbum.copyWith( deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection),
thumbnailId: NullableValue.valueOrEmpty(thumbnailId),
backupSelection: dbAlbum.backupSelection,
),
assetsToUpsert: newAssets, assetsToUpsert: newAssets,
); );
@ -187,7 +188,7 @@ class DeviceSyncService {
@visibleForTesting @visibleForTesting
// The [deviceAlbum] is expected to be refreshed before calling this method // The [deviceAlbum] is expected to be refreshed before calling this method
// with modified time and asset count // with modified time and asset count
Future<bool> fullSync(LocalAlbum dbAlbum, LocalAlbum deviceAlbum) async { Future<bool> fullDiff(LocalAlbum dbAlbum, LocalAlbum deviceAlbum) async {
try { try {
final assetsInDevice = deviceAlbum.assetCount > 0 final assetsInDevice = deviceAlbum.assetCount > 0
? await _albumMediaRepository.getAssetsForAlbum(deviceAlbum.id) ? await _albumMediaRepository.getAssetsForAlbum(deviceAlbum.id)
@ -201,23 +202,13 @@ class DeviceSyncService {
"Device album ${deviceAlbum.name} is empty. Removing assets from DB.", "Device album ${deviceAlbum.name} is empty. Removing assets from DB.",
); );
await _updateAlbum( await _updateAlbum(
deviceAlbum.copyWith( deviceAlbum.copyWith(backupSelection: dbAlbum.backupSelection),
// Clear thumbnail for empty album
thumbnailId: const NullableValue.empty(),
backupSelection: dbAlbum.backupSelection,
),
assetIdsToDelete: assetsInDb.map((a) => a.id), assetIdsToDelete: assetsInDb.map((a) => a.id),
); );
return true; 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( final updatedDeviceAlbum = deviceAlbum.copyWith(
thumbnailId: NullableValue.valueOrEmpty(thumbnailId),
backupSelection: dbAlbum.backupSelection, backupSelection: dbAlbum.backupSelection,
); );

View File

@ -1,7 +1,6 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/local_album.model.dart'; import 'package:immich_mobile/domain/models/local_album.model.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class LocalAlbumEntity extends Table with DriftDefaultsMixin { class LocalAlbumEntity extends Table with DriftDefaultsMixin {
@ -10,10 +9,8 @@ class LocalAlbumEntity extends Table with DriftDefaultsMixin {
TextColumn get id => text()(); TextColumn get id => text()();
TextColumn get name => text()(); TextColumn get name => text()();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
TextColumn get thumbnailId => text()
.nullable()
.references(LocalAssetEntity, #localId, onDelete: KeyAction.setNull)();
IntColumn get backupSelection => intEnum<BackupSelection>()(); IntColumn get backupSelection => intEnum<BackupSelection>()();
BoolColumn get marker_ => boolean().withDefault(const Constant(false))();
@override @override
Set<Column> get primaryKey => {id}; Set<Column> get primaryKey => {id};
@ -26,7 +23,6 @@ extension LocalAlbumEntityX on LocalAlbumEntityData {
name: name, name: name,
updatedAt: updatedAt, updatedAt: updatedAt,
assetCount: assetCount, assetCount: assetCount,
thumbnailId: thumbnailId,
backupSelection: backupSelection, backupSelection: backupSelection,
); );
} }

View File

@ -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' import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'
as i3; as i3;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; 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 typedef $$LocalAlbumEntityTableCreateCompanionBuilder
= i1.LocalAlbumEntityCompanion Function({ = i1.LocalAlbumEntityCompanion Function({
required String id, required String id,
required String name, required String name,
i0.Value<DateTime> updatedAt, i0.Value<DateTime> updatedAt,
i0.Value<String?> thumbnailId,
required i2.BackupSelection backupSelection, required i2.BackupSelection backupSelection,
i0.Value<bool> marker_,
}); });
typedef $$LocalAlbumEntityTableUpdateCompanionBuilder typedef $$LocalAlbumEntityTableUpdateCompanionBuilder
= i1.LocalAlbumEntityCompanion Function({ = i1.LocalAlbumEntityCompanion Function({
i0.Value<String> id, i0.Value<String> id,
i0.Value<String> name, i0.Value<String> name,
i0.Value<DateTime> updatedAt, i0.Value<DateTime> updatedAt,
i0.Value<String?> thumbnailId,
i0.Value<i2.BackupSelection> backupSelection, 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 class $$LocalAlbumEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAlbumEntityTable> { extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAlbumEntityTable> {
$$LocalAlbumEntityTableFilterComposer({ $$LocalAlbumEntityTableFilterComposer({
@ -83,27 +48,8 @@ class $$LocalAlbumEntityTableFilterComposer
column: $table.backupSelection, column: $table.backupSelection,
builder: (column) => i0.ColumnWithTypeConverterFilters(column)); builder: (column) => i0.ColumnWithTypeConverterFilters(column));
i5.$$LocalAssetEntityTableFilterComposer get thumbnailId { i0.ColumnFilters<bool> get marker_ => $composableBuilder(
final i5.$$LocalAssetEntityTableFilterComposer composer = $composerBuilder( column: $table.marker_, builder: (column) => i0.ColumnFilters(column));
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;
}
} }
class $$LocalAlbumEntityTableOrderingComposer class $$LocalAlbumEntityTableOrderingComposer
@ -129,29 +75,8 @@ class $$LocalAlbumEntityTableOrderingComposer
column: $table.backupSelection, column: $table.backupSelection,
builder: (column) => i0.ColumnOrderings(column)); builder: (column) => i0.ColumnOrderings(column));
i5.$$LocalAssetEntityTableOrderingComposer get thumbnailId { i0.ColumnOrderings<bool> get marker_ => $composableBuilder(
final i5.$$LocalAssetEntityTableOrderingComposer composer = column: $table.marker_, builder: (column) => i0.ColumnOrderings(column));
$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;
}
} }
class $$LocalAlbumEntityTableAnnotationComposer class $$LocalAlbumEntityTableAnnotationComposer
@ -176,29 +101,8 @@ class $$LocalAlbumEntityTableAnnotationComposer
get backupSelection => $composableBuilder( get backupSelection => $composableBuilder(
column: $table.backupSelection, builder: (column) => column); column: $table.backupSelection, builder: (column) => column);
i5.$$LocalAssetEntityTableAnnotationComposer get thumbnailId { i0.GeneratedColumn<bool> get marker_ =>
final i5.$$LocalAssetEntityTableAnnotationComposer composer = $composableBuilder(column: $table.marker_, builder: (column) => column);
$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;
}
} }
class $$LocalAlbumEntityTableTableManager extends i0.RootTableManager< class $$LocalAlbumEntityTableTableManager extends i0.RootTableManager<
@ -210,9 +114,13 @@ class $$LocalAlbumEntityTableTableManager extends i0.RootTableManager<
i1.$$LocalAlbumEntityTableAnnotationComposer, i1.$$LocalAlbumEntityTableAnnotationComposer,
$$LocalAlbumEntityTableCreateCompanionBuilder, $$LocalAlbumEntityTableCreateCompanionBuilder,
$$LocalAlbumEntityTableUpdateCompanionBuilder, $$LocalAlbumEntityTableUpdateCompanionBuilder,
(i1.LocalAlbumEntityData, i1.$$LocalAlbumEntityTableReferences), (
i1.LocalAlbumEntityData,
i0.BaseReferences<i0.GeneratedDatabase, i1.$LocalAlbumEntityTable,
i1.LocalAlbumEntityData>
),
i1.LocalAlbumEntityData, i1.LocalAlbumEntityData,
i0.PrefetchHooks Function({bool thumbnailId})> { i0.PrefetchHooks Function()> {
$$LocalAlbumEntityTableTableManager( $$LocalAlbumEntityTableTableManager(
i0.GeneratedDatabase db, i1.$LocalAlbumEntityTable table) i0.GeneratedDatabase db, i1.$LocalAlbumEntityTable table)
: super(i0.TableManagerState( : super(i0.TableManagerState(
@ -229,73 +137,35 @@ class $$LocalAlbumEntityTableTableManager extends i0.RootTableManager<
i0.Value<String> id = const i0.Value.absent(), i0.Value<String> id = const i0.Value.absent(),
i0.Value<String> name = const i0.Value.absent(), i0.Value<String> name = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = 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 = i0.Value<i2.BackupSelection> backupSelection =
const i0.Value.absent(), const i0.Value.absent(),
i0.Value<bool> marker_ = const i0.Value.absent(),
}) => }) =>
i1.LocalAlbumEntityCompanion( i1.LocalAlbumEntityCompanion(
id: id, id: id,
name: name, name: name,
updatedAt: updatedAt, updatedAt: updatedAt,
thumbnailId: thumbnailId,
backupSelection: backupSelection, backupSelection: backupSelection,
marker_: marker_,
), ),
createCompanionCallback: ({ createCompanionCallback: ({
required String id, required String id,
required String name, required String name,
i0.Value<DateTime> updatedAt = const i0.Value.absent(), i0.Value<DateTime> updatedAt = const i0.Value.absent(),
i0.Value<String?> thumbnailId = const i0.Value.absent(),
required i2.BackupSelection backupSelection, required i2.BackupSelection backupSelection,
i0.Value<bool> marker_ = const i0.Value.absent(),
}) => }) =>
i1.LocalAlbumEntityCompanion.insert( i1.LocalAlbumEntityCompanion.insert(
id: id, id: id,
name: name, name: name,
updatedAt: updatedAt, updatedAt: updatedAt,
thumbnailId: thumbnailId,
backupSelection: backupSelection, backupSelection: backupSelection,
marker_: marker_,
), ),
withReferenceMapper: (p0) => p0 withReferenceMapper: (p0) => p0
.map((e) => ( .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
e.readTable(table),
i1.$$LocalAlbumEntityTableReferences(db, table, e)
))
.toList(), .toList(),
prefetchHooksCallback: ({thumbnailId = false}) { prefetchHooksCallback: null,
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 [];
},
);
},
)); ));
} }
@ -308,9 +178,13 @@ typedef $$LocalAlbumEntityTableProcessedTableManager = i0.ProcessedTableManager<
i1.$$LocalAlbumEntityTableAnnotationComposer, i1.$$LocalAlbumEntityTableAnnotationComposer,
$$LocalAlbumEntityTableCreateCompanionBuilder, $$LocalAlbumEntityTableCreateCompanionBuilder,
$$LocalAlbumEntityTableUpdateCompanionBuilder, $$LocalAlbumEntityTableUpdateCompanionBuilder,
(i1.LocalAlbumEntityData, i1.$$LocalAlbumEntityTableReferences), (
i1.LocalAlbumEntityData,
i0.BaseReferences<i0.GeneratedDatabase, i1.$LocalAlbumEntityTable,
i1.LocalAlbumEntityData>
),
i1.LocalAlbumEntityData, i1.LocalAlbumEntityData,
i0.PrefetchHooks Function({bool thumbnailId})>; i0.PrefetchHooks Function()>;
class $LocalAlbumEntityTable extends i3.LocalAlbumEntity class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
with i0.TableInfo<$LocalAlbumEntityTable, i1.LocalAlbumEntityData> { with i0.TableInfo<$LocalAlbumEntityTable, i1.LocalAlbumEntityData> {
@ -337,15 +211,6 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
type: i0.DriftSqlType.dateTime, type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false, requiredDuringInsert: false,
defaultValue: i4.currentDateAndTime); 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 @override
late final i0.GeneratedColumnWithTypeConverter<i2.BackupSelection, int> late final i0.GeneratedColumnWithTypeConverter<i2.BackupSelection, int>
backupSelection = i0.GeneratedColumn<int>( backupSelection = i0.GeneratedColumn<int>(
@ -353,9 +218,19 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
type: i0.DriftSqlType.int, requiredDuringInsert: true) type: i0.DriftSqlType.int, requiredDuringInsert: true)
.withConverter<i2.BackupSelection>( .withConverter<i2.BackupSelection>(
i1.$LocalAlbumEntityTable.$converterbackupSelection); 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 @override
List<i0.GeneratedColumn> get $columns => List<i0.GeneratedColumn> get $columns =>
[id, name, updatedAt, thumbnailId, backupSelection]; [id, name, updatedAt, backupSelection, marker_];
@override @override
String get aliasedName => _alias ?? actualTableName; String get aliasedName => _alias ?? actualTableName;
@override @override
@ -382,11 +257,9 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
context.handle(_updatedAtMeta, context.handle(_updatedAtMeta,
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta));
} }
if (data.containsKey('thumbnail_id')) { if (data.containsKey('marker')) {
context.handle( context.handle(_marker_Meta,
_thumbnailIdMeta, marker_.isAcceptableOrUnknown(data['marker']!, _marker_Meta));
thumbnailId.isAcceptableOrUnknown(
data['thumbnail_id']!, _thumbnailIdMeta));
} }
return context; return context;
} }
@ -404,11 +277,11 @@ class $LocalAlbumEntityTable extends i3.LocalAlbumEntity
.read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!, .read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!,
updatedAt: attachedDatabase.typeMapping.read( updatedAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!,
thumbnailId: attachedDatabase.typeMapping
.read(i0.DriftSqlType.string, data['${effectivePrefix}thumbnail_id']),
backupSelection: i1.$LocalAlbumEntityTable.$converterbackupSelection backupSelection: i1.$LocalAlbumEntityTable.$converterbackupSelection
.fromSql(attachedDatabase.typeMapping.read(i0.DriftSqlType.int, .fromSql(attachedDatabase.typeMapping.read(i0.DriftSqlType.int,
data['${effectivePrefix}backup_selection'])!), 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 id;
final String name; final String name;
final DateTime updatedAt; final DateTime updatedAt;
final String? thumbnailId;
final i2.BackupSelection backupSelection; final i2.BackupSelection backupSelection;
final bool marker_;
const LocalAlbumEntityData( const LocalAlbumEntityData(
{required this.id, {required this.id,
required this.name, required this.name,
required this.updatedAt, required this.updatedAt,
this.thumbnailId, required this.backupSelection,
required this.backupSelection}); required this.marker_});
@override @override
Map<String, i0.Expression> toColumns(bool nullToAbsent) { Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{}; final map = <String, i0.Expression>{};
map['id'] = i0.Variable<String>(id); map['id'] = i0.Variable<String>(id);
map['name'] = i0.Variable<String>(name); map['name'] = i0.Variable<String>(name);
map['updated_at'] = i0.Variable<DateTime>(updatedAt); 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 map['backup_selection'] = i0.Variable<int>(i1
.$LocalAlbumEntityTable.$converterbackupSelection .$LocalAlbumEntityTable.$converterbackupSelection
.toSql(backupSelection)); .toSql(backupSelection));
} }
map['marker'] = i0.Variable<bool>(marker_);
return map; return map;
} }
@ -464,9 +335,9 @@ class LocalAlbumEntityData extends i0.DataClass
id: serializer.fromJson<String>(json['id']), id: serializer.fromJson<String>(json['id']),
name: serializer.fromJson<String>(json['name']), name: serializer.fromJson<String>(json['name']),
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']), updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
thumbnailId: serializer.fromJson<String?>(json['thumbnailId']),
backupSelection: i1.$LocalAlbumEntityTable.$converterbackupSelection backupSelection: i1.$LocalAlbumEntityTable.$converterbackupSelection
.fromJson(serializer.fromJson<int>(json['backupSelection'])), .fromJson(serializer.fromJson<int>(json['backupSelection'])),
marker_: serializer.fromJson<bool>(json['marker_']),
); );
} }
@override @override
@ -476,10 +347,10 @@ class LocalAlbumEntityData extends i0.DataClass
'id': serializer.toJson<String>(id), 'id': serializer.toJson<String>(id),
'name': serializer.toJson<String>(name), 'name': serializer.toJson<String>(name),
'updatedAt': serializer.toJson<DateTime>(updatedAt), 'updatedAt': serializer.toJson<DateTime>(updatedAt),
'thumbnailId': serializer.toJson<String?>(thumbnailId),
'backupSelection': serializer.toJson<int>(i1 'backupSelection': serializer.toJson<int>(i1
.$LocalAlbumEntityTable.$converterbackupSelection .$LocalAlbumEntityTable.$converterbackupSelection
.toJson(backupSelection)), .toJson(backupSelection)),
'marker_': serializer.toJson<bool>(marker_),
}; };
} }
@ -487,25 +358,24 @@ class LocalAlbumEntityData extends i0.DataClass
{String? id, {String? id,
String? name, String? name,
DateTime? updatedAt, DateTime? updatedAt,
i0.Value<String?> thumbnailId = const i0.Value.absent(), i2.BackupSelection? backupSelection,
i2.BackupSelection? backupSelection}) => bool? marker_}) =>
i1.LocalAlbumEntityData( i1.LocalAlbumEntityData(
id: id ?? this.id, id: id ?? this.id,
name: name ?? this.name, name: name ?? this.name,
updatedAt: updatedAt ?? this.updatedAt, updatedAt: updatedAt ?? this.updatedAt,
thumbnailId: thumbnailId.present ? thumbnailId.value : this.thumbnailId,
backupSelection: backupSelection ?? this.backupSelection, backupSelection: backupSelection ?? this.backupSelection,
marker_: marker_ ?? this.marker_,
); );
LocalAlbumEntityData copyWithCompanion(i1.LocalAlbumEntityCompanion data) { LocalAlbumEntityData copyWithCompanion(i1.LocalAlbumEntityCompanion data) {
return LocalAlbumEntityData( return LocalAlbumEntityData(
id: data.id.present ? data.id.value : this.id, id: data.id.present ? data.id.value : this.id,
name: data.name.present ? data.name.value : this.name, name: data.name.present ? data.name.value : this.name,
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
thumbnailId:
data.thumbnailId.present ? data.thumbnailId.value : this.thumbnailId,
backupSelection: data.backupSelection.present backupSelection: data.backupSelection.present
? data.backupSelection.value ? data.backupSelection.value
: this.backupSelection, : this.backupSelection,
marker_: data.marker_.present ? data.marker_.value : this.marker_,
); );
} }
@ -515,15 +385,15 @@ class LocalAlbumEntityData extends i0.DataClass
..write('id: $id, ') ..write('id: $id, ')
..write('name: $name, ') ..write('name: $name, ')
..write('updatedAt: $updatedAt, ') ..write('updatedAt: $updatedAt, ')
..write('thumbnailId: $thumbnailId, ') ..write('backupSelection: $backupSelection, ')
..write('backupSelection: $backupSelection') ..write('marker_: $marker_')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@override @override
int get hashCode => int get hashCode =>
Object.hash(id, name, updatedAt, thumbnailId, backupSelection); Object.hash(id, name, updatedAt, backupSelection, marker_);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
@ -531,8 +401,8 @@ class LocalAlbumEntityData extends i0.DataClass
other.id == this.id && other.id == this.id &&
other.name == this.name && other.name == this.name &&
other.updatedAt == this.updatedAt && other.updatedAt == this.updatedAt &&
other.thumbnailId == this.thumbnailId && other.backupSelection == this.backupSelection &&
other.backupSelection == this.backupSelection); other.marker_ == this.marker_);
} }
class LocalAlbumEntityCompanion class LocalAlbumEntityCompanion
@ -540,21 +410,21 @@ class LocalAlbumEntityCompanion
final i0.Value<String> id; final i0.Value<String> id;
final i0.Value<String> name; final i0.Value<String> name;
final i0.Value<DateTime> updatedAt; final i0.Value<DateTime> updatedAt;
final i0.Value<String?> thumbnailId;
final i0.Value<i2.BackupSelection> backupSelection; final i0.Value<i2.BackupSelection> backupSelection;
final i0.Value<bool> marker_;
const LocalAlbumEntityCompanion({ const LocalAlbumEntityCompanion({
this.id = const i0.Value.absent(), this.id = const i0.Value.absent(),
this.name = const i0.Value.absent(), this.name = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(), this.updatedAt = const i0.Value.absent(),
this.thumbnailId = const i0.Value.absent(),
this.backupSelection = const i0.Value.absent(), this.backupSelection = const i0.Value.absent(),
this.marker_ = const i0.Value.absent(),
}); });
LocalAlbumEntityCompanion.insert({ LocalAlbumEntityCompanion.insert({
required String id, required String id,
required String name, required String name,
this.updatedAt = const i0.Value.absent(), this.updatedAt = const i0.Value.absent(),
this.thumbnailId = const i0.Value.absent(),
required i2.BackupSelection backupSelection, required i2.BackupSelection backupSelection,
this.marker_ = const i0.Value.absent(),
}) : id = i0.Value(id), }) : id = i0.Value(id),
name = i0.Value(name), name = i0.Value(name),
backupSelection = i0.Value(backupSelection); backupSelection = i0.Value(backupSelection);
@ -562,15 +432,15 @@ class LocalAlbumEntityCompanion
i0.Expression<String>? id, i0.Expression<String>? id,
i0.Expression<String>? name, i0.Expression<String>? name,
i0.Expression<DateTime>? updatedAt, i0.Expression<DateTime>? updatedAt,
i0.Expression<String>? thumbnailId,
i0.Expression<int>? backupSelection, i0.Expression<int>? backupSelection,
i0.Expression<bool>? marker_,
}) { }) {
return i0.RawValuesInsertable({ return i0.RawValuesInsertable({
if (id != null) 'id': id, if (id != null) 'id': id,
if (name != null) 'name': name, if (name != null) 'name': name,
if (updatedAt != null) 'updated_at': updatedAt, if (updatedAt != null) 'updated_at': updatedAt,
if (thumbnailId != null) 'thumbnail_id': thumbnailId,
if (backupSelection != null) 'backup_selection': backupSelection, if (backupSelection != null) 'backup_selection': backupSelection,
if (marker_ != null) 'marker': marker_,
}); });
} }
@ -578,14 +448,14 @@ class LocalAlbumEntityCompanion
{i0.Value<String>? id, {i0.Value<String>? id,
i0.Value<String>? name, i0.Value<String>? name,
i0.Value<DateTime>? updatedAt, 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( return i1.LocalAlbumEntityCompanion(
id: id ?? this.id, id: id ?? this.id,
name: name ?? this.name, name: name ?? this.name,
updatedAt: updatedAt ?? this.updatedAt, updatedAt: updatedAt ?? this.updatedAt,
thumbnailId: thumbnailId ?? this.thumbnailId,
backupSelection: backupSelection ?? this.backupSelection, backupSelection: backupSelection ?? this.backupSelection,
marker_: marker_ ?? this.marker_,
); );
} }
@ -601,14 +471,14 @@ class LocalAlbumEntityCompanion
if (updatedAt.present) { if (updatedAt.present) {
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value); map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
} }
if (thumbnailId.present) {
map['thumbnail_id'] = i0.Variable<String>(thumbnailId.value);
}
if (backupSelection.present) { if (backupSelection.present) {
map['backup_selection'] = i0.Variable<int>(i1 map['backup_selection'] = i0.Variable<int>(i1
.$LocalAlbumEntityTable.$converterbackupSelection .$LocalAlbumEntityTable.$converterbackupSelection
.toSql(backupSelection.value)); .toSql(backupSelection.value));
} }
if (marker_.present) {
map['marker'] = i0.Variable<bool>(marker_.value);
}
return map; return map;
} }
@ -618,8 +488,8 @@ class LocalAlbumEntityCompanion
..write('id: $id, ') ..write('id: $id, ')
..write('name: $name, ') ..write('name: $name, ')
..write('updatedAt: $updatedAt, ') ..write('updatedAt: $updatedAt, ')
..write('thumbnailId: $thumbnailId, ') ..write('backupSelection: $backupSelection, ')
..write('backupSelection: $backupSelection') ..write('marker_: $marker_')
..write(')')) ..write(')'))
.toString(); .toString();
} }

View File

@ -41,12 +41,10 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
@override @override
Future<List<LocalAlbum>> getAll() { Future<List<LocalAlbum>> getAll() {
final filter = AdvancedCustomFilter( return PhotoManager.getAssetPathList(
orderBy: [OrderByItem.asc(CustomColumns.base.id)], hasAll: true,
); filterOption: AdvancedCustomFilter(),
).then((e) {
return PhotoManager.getAssetPathList(hasAll: true, filterOption: filter)
.then((e) {
if (_platform.isAndroid) { if (_platform.isAndroid) {
e.removeWhere((a) => a.isAll); e.removeWhere((a) => a.isAll);
} }

View File

@ -7,9 +7,9 @@ import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift
as i2; as i2;
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
as i3; 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' 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; as i5;
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart' import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
as i6; as i6;
@ -26,10 +26,10 @@ abstract class $Drift extends i0.GeneratedDatabase {
i2.$UserMetadataEntityTable(this); i2.$UserMetadataEntityTable(this);
late final i3.$PartnerEntityTable partnerEntity = late final i3.$PartnerEntityTable partnerEntity =
i3.$PartnerEntityTable(this); i3.$PartnerEntityTable(this);
late final i4.$LocalAssetEntityTable localAssetEntity = late final i4.$LocalAlbumEntityTable localAlbumEntity =
i4.$LocalAssetEntityTable(this); i4.$LocalAlbumEntityTable(this);
late final i5.$LocalAlbumEntityTable localAlbumEntity = late final i5.$LocalAssetEntityTable localAssetEntity =
i5.$LocalAlbumEntityTable(this); i5.$LocalAssetEntityTable(this);
late final i6.$LocalAlbumAssetEntityTable localAlbumAssetEntity = late final i6.$LocalAlbumAssetEntityTable localAlbumAssetEntity =
i6.$LocalAlbumAssetEntityTable(this); i6.$LocalAlbumAssetEntityTable(this);
late final i7.$RemoteAssetEntityTable remoteAssetEntity = late final i7.$RemoteAssetEntityTable remoteAssetEntity =
@ -43,12 +43,12 @@ abstract class $Drift extends i0.GeneratedDatabase {
userEntity, userEntity,
userMetadataEntity, userMetadataEntity,
partnerEntity, partnerEntity,
localAssetEntity,
localAlbumEntity, localAlbumEntity,
localAssetEntity,
localAlbumAssetEntity, localAlbumAssetEntity,
remoteAssetEntity, remoteAssetEntity,
exifEntity, exifEntity,
i4.localAssetChecksum, i5.localAssetChecksum,
i7.remoteAssetChecksum i7.remoteAssetChecksum
]; ];
@override @override
@ -77,13 +77,6 @@ abstract class $Drift extends i0.GeneratedDatabase {
i0.TableUpdate('partner_entity', kind: i0.UpdateKind.delete), 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( i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName('local_asset_entity', on: i0.TableUpdateQuery.onTableName('local_asset_entity',
limitUpdateKind: i0.UpdateKind.delete), limitUpdateKind: i0.UpdateKind.delete),
@ -130,10 +123,10 @@ class $DriftManager {
i2.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity); i2.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
i3.$$PartnerEntityTableTableManager get partnerEntity => i3.$$PartnerEntityTableTableManager get partnerEntity =>
i3.$$PartnerEntityTableTableManager(_db, _db.partnerEntity); i3.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
i4.$$LocalAssetEntityTableTableManager get localAssetEntity => i4.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
i4.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity); i4.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
i5.$$LocalAlbumEntityTableTableManager get localAlbumEntity => i5.$$LocalAssetEntityTableTableManager get localAssetEntity =>
i5.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity); i5.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity);
i6.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i6 i6.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i6
.$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity); .$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity);
i7.$$RemoteAssetEntityTableTableManager get remoteAssetEntity => i7.$$RemoteAssetEntityTableTableManager get remoteAssetEntity =>

View File

@ -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.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/platform/messages.g.dart' as platform;
import 'package:platform/platform.dart'; import 'package:platform/platform.dart';
class DriftLocalAlbumRepository extends DriftDatabaseRepository class DriftLocalAlbumRepository extends DriftDatabaseRepository
@ -65,7 +66,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
transaction(() async { transaction(() async {
await _upsertAssets(assets); await _upsertAssets(assets);
// Needs to be after asset upsert to link the thumbnail // Needs to be after asset upsert to link the thumbnail
await _upsertAlbum(localAlbum); await update(localAlbum);
await _linkAssetsToAlbum(localAlbum.id, assets); await _linkAssetsToAlbum(localAlbum.id, assets);
}); });
@ -112,7 +113,46 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
} }
@override @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 @override
Future<List<LocalAsset>> getAssetsForAlbum(String albumId) { Future<List<LocalAsset>> getAssetsForAlbum(String albumId) {
@ -132,17 +172,30 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
.get(); .get();
} }
Future<void> _upsertAlbum(LocalAlbum localAlbum) { @override
final companion = LocalAlbumEntityCompanion.insert( Future<void> handleSyncDelta(platform.SyncDelta delta) {
id: localAlbum.id, return _db.transaction(() async {
name: localAlbum.name, await _deleteAssets(delta.deletes);
updatedAt: Value(localAlbum.updatedAt),
thumbnailId: Value.absentIfNull(localAlbum.thumbnailId),
backupSelection: localAlbum.backupSelection,
);
return _db.localAlbumEntity await _upsertAssets(delta.updates.map((a) => a.toLocalAsset()));
.insertOne(companion, onConflict: DoUpdate((_) => companion)); 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( 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,
);
}
}

View File

@ -9,8 +9,8 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository
const DriftLocalAssetRepository(this._db) : super(_db); const DriftLocalAssetRepository(this._db) : super(_db);
@override @override
Future<LocalAsset> get(String assetId) => _db.managers.localAssetEntity Future<LocalAsset> get(String id) => _db.managers.localAssetEntity
.filter((f) => f.localId(assetId)) .filter((f) => f.localId(id))
.map((a) => a.toDto()) .map((a) => a.toDto())
.getSingle(); .getSingle();
} }

View 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
View 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;
}
}
}

View File

@ -0,0 +1,4 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/platform/messages.g.dart';
final platformMessagesImpl = Provider<ImHostService>((_) => ImHostService());

View File

@ -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/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.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/cancel.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
final deviceSyncServiceProvider = Provider( final deviceSyncServiceProvider = Provider(
(ref) => DeviceSyncService( (ref) => DeviceSyncService(
albumMediaRepository: ref.watch(albumMediaRepositoryProvider), albumMediaRepository: ref.watch(albumMediaRepositoryProvider),
localAlbumRepository: ref.watch(localAlbumRepository), localAlbumRepository: ref.watch(localAlbumRepository),
localAssetRepository: ref.watch(localAssetProvider), hostService: ref.watch(platformMessagesImpl),
), ),
); );

File diff suppressed because it is too large Load Diff

View File

@ -5,31 +5,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _fe_analyzer_shared name: _fe_analyzer_shared
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "76.0.0" version: "80.0.0"
_macros:
dependency: transitive
description: dart
source: sdk
version: "0.3.3"
analyzer: analyzer:
dependency: "direct overridden" dependency: transitive
description: description:
name: analyzer name: analyzer
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.11.0" version: "7.3.0"
analyzer_plugin: analyzer_plugin:
dependency: "direct overridden" dependency: transitive
description: description:
name: analyzer_plugin name: analyzer_plugin
sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.3" version: "0.13.0"
ansicolor: ansicolor:
dependency: transitive dependency: transitive
description: description:
@ -74,10 +69,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: auto_route_generator name: auto_route_generator
sha256: c9086eb07271e51b44071ad5cff34e889f3156710b964a308c2ab590769e79e6 sha256: c2e359d8932986d4d1bcad7a428143f81384ce10fef8d4aa5bc29e1f83766a46
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.0.0" version: "9.3.1"
background_downloader: background_downloader:
dependency: "direct main" dependency: "direct main"
description: description:
@ -322,34 +317,42 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: custom_lint name: custom_lint
sha256: "4500e88854e7581ee43586abeaf4443cb22375d6d289241a87b1aadf678d5545" sha256: "409c485fd14f544af1da965d5a0d160ee57cd58b63eeaa7280a4f28cf5bda7f1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.10" version: "0.7.5"
custom_lint_builder: custom_lint_builder:
dependency: transitive dependency: transitive
description: description:
name: custom_lint_builder name: custom_lint_builder
sha256: "5a95eff100da256fbf086b329c17c8b49058c261cdf56d3a4157d3c31c511d78" sha256: "107e0a43606138015777590ee8ce32f26ba7415c25b722ff0908a6f5d7a4c228"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.10" version: "0.7.5"
custom_lint_core: custom_lint_core:
dependency: transitive dependency: transitive
description: description:
name: custom_lint_core name: custom_lint_core
sha256: "76a4046cc71d976222a078a8fd4a65e198b70545a8d690a75196dd14f08510f6" sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted 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: dart_style:
dependency: transitive dependency: transitive
description: description:
name: dart_style name: dart_style
sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.8" version: "3.0.1"
dartx: dartx:
dependency: transitive dependency: transitive
description: description:
@ -675,10 +678,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: freezed_annotation name: freezed_annotation
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.4" version: "3.0.0"
frontend_server_client: frontend_server_client:
dependency: transitive dependency: transitive
description: description:
@ -923,10 +926,11 @@ packages:
isar_generator: isar_generator:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: isar_generator path: "packages/isar_generator"
sha256: "484e73d3b7e81dbd816852fe0b9497333118a9aeb646fd2d349a62cc8980ffe1" ref: v3
url: "https://pub.isar-community.dev" resolved-ref: ad574f60ed6f39d2995cd16fc7dc3de9a646ef30
source: hosted url: "https://github.com/callumw-k/isar"
source: git
version: "3.1.8" version: "3.1.8"
js: js:
dependency: transitive dependency: transitive
@ -984,14 +988,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" 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: maplibre_gl:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1033,7 +1029,7 @@ packages:
source: hosted source: hosted
version: "0.11.1" version: "0.11.1"
meta: meta:
dependency: "direct overridden" dependency: transitive
description: description:
name: meta name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
@ -1264,6 +1260,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
pigeon:
dependency: "direct dev"
description:
name: pigeon
sha256: "3e4e6258f22760fa11f86d2a5202fb3f8367cb361d33bd9a93de85a7959e9976"
url: "https://pub.dev"
source: hosted
version: "25.3.1"
platform: platform:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1348,10 +1352,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: riverpod_analyzer_utils name: riverpod_analyzer_utils
sha256: "0dcb0af32d561f8fa000c6a6d95633c9fb08ea8a8df46e3f9daca59f11218167" sha256: "03a17170088c63aab6c54c44456f5ab78876a1ddb6032ffde1662ddab4959611"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.6" version: "0.5.10"
riverpod_annotation: riverpod_annotation:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1364,18 +1368,18 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: riverpod_generator name: riverpod_generator
sha256: "851aedac7ad52693d12af3bf6d92b1626d516ed6b764eb61bf19e968b5e0b931" sha256: "44a0992d54473eb199ede00e2260bd3c262a86560e3c6f6374503d86d0580e36"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.6.1" version: "2.6.5"
riverpod_lint: riverpod_lint:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: riverpod_lint name: riverpod_lint
sha256: "0684c21a9a4582c28c897d55c7b611fa59a351579061b43f8c92c005804e63a8" sha256: "89a52b7334210dbff8605c3edf26cfe69b15062beed5cbfeff2c3812c33c9e35"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.6.1" version: "2.6.5"
rxdart: rxdart:
dependency: transitive dependency: transitive
description: description:
@ -1537,10 +1541,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_gen name: source_gen
sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.5.0" version: "2.0.0"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:

View File

@ -82,11 +82,6 @@ dependencies:
drift: ^2.23.1 drift: ^2.23.1
drift_flutter: ^0.2.4 drift_flutter: ^0.2.4
dependency_overrides:
analyzer: ^6.0.0
meta: ^1.11.0
analyzer_plugin: ^0.11.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
@ -96,11 +91,13 @@ dev_dependencies:
flutter_launcher_icons: ^0.14.3 flutter_launcher_icons: ^0.14.3
flutter_native_splash: ^2.4.5 flutter_native_splash: ^2.4.5
isar_generator: isar_generator:
version: *isar_version git:
hosted: https://pub.isar-community.dev/ url: https://github.com/callumw-k/isar
ref: v3
path: packages/isar_generator/
integration_test: integration_test:
sdk: flutter sdk: flutter
custom_lint: ^0.6.4 custom_lint: ^0.7.5
riverpod_lint: ^2.6.1 riverpod_lint: ^2.6.1
riverpod_generator: ^2.6.1 riverpod_generator: ^2.6.1
mocktail: ^1.0.4 mocktail: ^1.0.4
@ -110,6 +107,8 @@ dev_dependencies:
file: ^7.0.1 # for MemoryFileSystem file: ^7.0.1 # for MemoryFileSystem
# Drift generator # Drift generator
drift_dev: ^2.23.1 drift_dev: ^2.23.1
# Type safe platform code
pigeon: ^25.3.1
flutter: flutter:
uses-material-design: true uses-material-design: true

View File

@ -1,6 +1,7 @@
import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart';
import 'package:immich_mobile/platform/messages.g.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
class MockStoreService extends Mock implements StoreService {} class MockStoreService extends Mock implements StoreService {}
@ -8,3 +9,5 @@ class MockStoreService extends Mock implements StoreService {}
class MockUserService extends Mock implements UserService {} class MockUserService extends Mock implements UserService {}
class MockBackgroundSyncManager extends Mock implements BackgroundSyncManager {} class MockBackgroundSyncManager extends Mock implements BackgroundSyncManager {}
class MockHostService extends Mock implements ImHostService {}

View File

@ -2,21 +2,21 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/domain/interfaces/album_media.interface.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_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/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/local_album.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/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 'package:mocktail/mocktail.dart';
import '../../fixtures/local_album.stub.dart'; import '../../fixtures/local_album.stub.dart';
import '../../fixtures/local_asset.stub.dart'; import '../../fixtures/local_asset.stub.dart';
import '../../infrastructure/repository.mock.dart'; import '../../infrastructure/repository.mock.dart';
import '../service.mock.dart';
void main() { void main() {
late IAlbumMediaRepository mockAlbumMediaRepo; late IAlbumMediaRepository mockAlbumMediaRepo;
late ILocalAlbumRepository mockLocalAlbumRepo; late ILocalAlbumRepository mockLocalAlbumRepo;
late ILocalAssetRepository mockLocalAssetRepo; late ImHostService mockHostService;
late DeviceSyncService sut; late DeviceSyncService sut;
Future<T> mockTransaction<T>(Future<T> Function() action) => action(); Future<T> mockTransaction<T>(Future<T> Function() action) => action();
@ -24,12 +24,12 @@ void main() {
setUp(() { setUp(() {
mockAlbumMediaRepo = MockAlbumMediaRepository(); mockAlbumMediaRepo = MockAlbumMediaRepository();
mockLocalAlbumRepo = MockLocalAlbumRepository(); mockLocalAlbumRepo = MockLocalAlbumRepository();
mockLocalAssetRepo = MockLocalAssetRepository(); mockHostService = MockHostService();
sut = DeviceSyncService( sut = DeviceSyncService(
albumMediaRepository: mockAlbumMediaRepo, albumMediaRepository: mockAlbumMediaRepo,
localAlbumRepository: mockLocalAlbumRepo, localAlbumRepository: mockLocalAlbumRepo,
localAssetRepository: mockLocalAssetRepo, hostService: mockHostService,
); );
registerFallbackValue(LocalAlbumStub.album1); registerFallbackValue(LocalAlbumStub.album1);
@ -51,8 +51,6 @@ void main() {
.thenAnswer((_) async => true); .thenAnswer((_) async => true);
when(() => mockLocalAlbumRepo.getAssetsForAlbum(any())) when(() => mockLocalAlbumRepo.getAssetsForAlbum(any()))
.thenAnswer((_) async => []); .thenAnswer((_) async => []);
when(() => mockLocalAssetRepo.get(any()))
.thenAnswer((_) async => LocalAssetStub.image1);
when(() => mockAlbumMediaRepo.refresh(any())).thenAnswer( when(() => mockAlbumMediaRepo.refresh(any())).thenAnswer(
(inv) async => (inv) async =>
LocalAlbumStub.album1.copyWith(id: inv.positionalArguments.first), LocalAlbumStub.album1.copyWith(id: inv.positionalArguments.first),
@ -250,7 +248,7 @@ void main() {
group('addAlbum', () { group('addAlbum', () {
test( test(
'refreshes, gets assets, sets thumbnail, and inserts for non-empty album', 'refreshes, gets assets, and inserts for non-empty album',
() async { () async {
final newAlbum = LocalAlbumStub.album1.copyWith(assetCount: 0); final newAlbum = LocalAlbumStub.album1.copyWith(assetCount: 0);
final refreshedAlbum = final refreshedAlbum =
@ -284,38 +282,33 @@ void main() {
expect(capturedAlbum.id, newAlbum.id); expect(capturedAlbum.id, newAlbum.id);
expect(capturedAlbum.assetCount, refreshedAlbum.assetCount); expect(capturedAlbum.assetCount, refreshedAlbum.assetCount);
expect(capturedAlbum.updatedAt, refreshedAlbum.updatedAt); expect(capturedAlbum.updatedAt, refreshedAlbum.updatedAt);
expect(capturedAlbum.thumbnailId, assets.first.id);
expect(listEquals(capturedAssets, assets), isTrue); expect(listEquals(capturedAssets, assets), isTrue);
}, },
); );
test( test('refreshes, skips assets, and inserts for empty album', () async {
'refreshes, skips assets, sets null thumbnail, and inserts for empty album', final newAlbum = LocalAlbumStub.album1.copyWith(assetCount: 0);
() async { final refreshedAlbum =
final newAlbum = LocalAlbumStub.album1.copyWith(assetCount: 0); newAlbum.copyWith(updatedAt: DateTime(2024), assetCount: 0);
final refreshedAlbum =
newAlbum.copyWith(updatedAt: DateTime(2024), assetCount: 0);
when(() => mockAlbumMediaRepo.refresh(newAlbum.id)) when(() => mockAlbumMediaRepo.refresh(newAlbum.id))
.thenAnswer((_) async => refreshedAlbum); .thenAnswer((_) async => refreshedAlbum);
await sut.addAlbum(newAlbum); await sut.addAlbum(newAlbum);
verify(() => mockAlbumMediaRepo.refresh(newAlbum.id)).called(1); verify(() => mockAlbumMediaRepo.refresh(newAlbum.id)).called(1);
verifyNever(() => mockAlbumMediaRepo.getAssetsForAlbum(newAlbum.id)); verifyNever(() => mockAlbumMediaRepo.getAssetsForAlbum(newAlbum.id));
final captured = final captured =
verify(() => mockLocalAlbumRepo.insert(captureAny(), captureAny())) verify(() => mockLocalAlbumRepo.insert(captureAny(), captureAny()))
.captured; .captured;
final capturedAlbum = captured.first as LocalAlbum; final capturedAlbum = captured.first as LocalAlbum;
final capturedAssets = captured[1] as List<Object?>; final capturedAssets = captured[1] as List<Object?>;
expect(capturedAlbum.id, newAlbum.id); expect(capturedAlbum.id, newAlbum.id);
expect(capturedAlbum.assetCount, 0); expect(capturedAlbum.assetCount, 0);
expect(capturedAlbum.thumbnailId, isNull); expect(capturedAssets, isEmpty);
expect(capturedAssets, isEmpty); });
},
);
}); });
group('removeAlbum', () { group('removeAlbum', () {
@ -361,11 +354,7 @@ void main() {
updateTimeCond: any(named: 'updateTimeCond'), updateTimeCond: any(named: 'updateTimeCond'),
), ),
).thenAnswer((_) async => [newAsset]); ).thenAnswer((_) async => [newAsset]);
final dbAlbumNoThumb = final result = await sut.updateAlbum(dbAlbum, LocalAlbumStub.album1);
dbAlbum.copyWith(thumbnailId: const NullableValue.absent());
final result =
await sut.updateAlbum(dbAlbumNoThumb, LocalAlbumStub.album1);
expect(result, isTrue); expect(result, isTrue);
verify(() => mockAlbumMediaRepo.refresh(dbAlbum.id)).called(1); verify(() => mockAlbumMediaRepo.refresh(dbAlbum.id)).called(1);
@ -376,7 +365,6 @@ void main() {
updateTimeCond: any(named: 'updateTimeCond'), updateTimeCond: any(named: 'updateTimeCond'),
), ),
).called(1); ).called(1);
verifyNever(() => mockLocalAssetRepo.get(any()));
verify(() => mockLocalAlbumRepo.transaction<void>(any())).called(1); verify(() => mockLocalAlbumRepo.transaction<void>(any())).called(1);
verify(() => mockLocalAlbumRepo.addAssets(dbAlbum.id, [newAsset])) verify(() => mockLocalAlbumRepo.addAssets(dbAlbum.id, [newAsset]))
@ -385,10 +373,7 @@ void main() {
() => mockLocalAlbumRepo.update( () => mockLocalAlbumRepo.update(
any( any(
that: predicate<LocalAlbum>( that: predicate<LocalAlbum>(
(a) => (a) => a.id == dbAlbum.id && a.assetCount == 2,
a.id == dbAlbum.id &&
a.assetCount == 2 &&
a.thumbnailId == newAsset.id,
), ),
), ),
), ),
@ -437,10 +422,7 @@ void main() {
() => mockLocalAlbumRepo.update( () => mockLocalAlbumRepo.update(
any( any(
that: predicate<LocalAlbum>( that: predicate<LocalAlbum>(
(a) => (a) => a.id == dbAlbum.id && a.assetCount == 0,
a.id == dbAlbum.id &&
a.assetCount == 0 &&
a.thumbnailId == null,
), ),
), ),
), ),
@ -508,11 +490,8 @@ void main() {
}); });
group('checkAddition', () { group('checkAddition', () {
final dbAlbum = LocalAlbumStub.album1.copyWith( final dbAlbum = LocalAlbumStub.album1
updatedAt: DateTime(2024, 1, 1, 10, 0, 0), .copyWith(updatedAt: DateTime(2024, 1, 1, 10, 0, 0), assetCount: 1);
assetCount: 1,
thumbnailId: const NullableValue.value("thumb1"),
);
final refreshedAlbum = dbAlbum.copyWith( final refreshedAlbum = dbAlbum.copyWith(
updatedAt: DateTime(2024, 1, 1, 11, 0, 0), updatedAt: DateTime(2024, 1, 1, 11, 0, 0),
assetCount: 2, assetCount: 2,
@ -530,13 +509,6 @@ void main() {
), ),
).thenAnswer((_) async => [newAsset]); ).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); final result = await sut.checkAddition(dbAlbum, refreshedAlbum);
expect(result, isTrue); expect(result, isTrue);
@ -546,19 +518,15 @@ void main() {
updateTimeCond: any(named: 'updateTimeCond'), updateTimeCond: any(named: 'updateTimeCond'),
), ),
).called(1); ).called(1);
verify(() => mockLocalAssetRepo.get("thumb1")).called(1);
verify(() => mockLocalAlbumRepo.addAssets(dbAlbum.id, [newAsset])) verify(() => mockLocalAlbumRepo.addAssets(dbAlbum.id, [newAsset]))
.called(1); .called(1);
verify( verify(
() => mockLocalAlbumRepo.update( () => mockLocalAlbumRepo.update(
any( any(
that: predicate<LocalAlbum>( that: predicate<LocalAlbum>((a) =>
(a) => a.id == dbAlbum.id &&
a.id == dbAlbum.id && a.assetCount == 2 &&
a.assetCount == 2 && a.updatedAt == refreshedAlbum.updatedAt),
a.updatedAt == refreshedAlbum.updatedAt &&
a.thumbnailId == newAsset.id,
),
), ),
), ),
).called(1); ).called(1);
@ -566,93 +534,6 @@ void main() {
.called(1); .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 { test('returns false if assetCount decreased', () async {
final decreasedCountAlbum = refreshedAlbum.copyWith(assetCount: 0); final decreasedCountAlbum = refreshedAlbum.copyWith(assetCount: 0);
final result = await sut.checkAddition(dbAlbum, decreasedCountAlbum); final result = await sut.checkAddition(dbAlbum, decreasedCountAlbum);
@ -725,7 +606,6 @@ void main() {
final dbAlbum = LocalAlbumStub.album1.copyWith( final dbAlbum = LocalAlbumStub.album1.copyWith(
updatedAt: DateTime(2024, 1, 1), updatedAt: DateTime(2024, 1, 1),
assetCount: 2, assetCount: 2,
thumbnailId: const NullableValue.value("asset1"),
); );
final refreshedAlbum = dbAlbum.copyWith( final refreshedAlbum = dbAlbum.copyWith(
updatedAt: DateTime(2024, 1, 2), updatedAt: DateTime(2024, 1, 2),
@ -760,7 +640,7 @@ void main() {
when(() => mockLocalAlbumRepo.getAssetsForAlbum(dbAlbum.id)) when(() => mockLocalAlbumRepo.getAssetsForAlbum(dbAlbum.id))
.thenAnswer((_) async => [dbAsset1, dbAsset2]); .thenAnswer((_) async => [dbAsset1, dbAsset2]);
final result = await sut.fullSync(dbAlbum, emptyRefreshedAlbum); final result = await sut.fullDiff(dbAlbum, emptyRefreshedAlbum);
expect(result, isTrue); expect(result, isTrue);
verifyNever( verifyNever(
@ -773,13 +653,10 @@ void main() {
verify( verify(
() => mockLocalAlbumRepo.update( () => mockLocalAlbumRepo.update(
any( any(
that: predicate<LocalAlbum>( that: predicate<LocalAlbum>((a) =>
(a) => a.id == dbAlbum.id &&
a.id == dbAlbum.id && a.assetCount == 0 &&
a.assetCount == 0 && a.updatedAt == emptyRefreshedAlbum.updatedAt),
a.updatedAt == emptyRefreshedAlbum.updatedAt &&
a.thumbnailId == null,
),
), ),
), ),
).called(1); ).called(1);
@ -789,10 +666,7 @@ void main() {
}); });
test('handles empty DB album -> adds all device assets', () async { test('handles empty DB album -> adds all device assets', () async {
final emptyDbAlbum = dbAlbum.copyWith( final emptyDbAlbum = dbAlbum.copyWith(assetCount: 0);
assetCount: 0,
thumbnailId: const NullableValue.empty(),
);
final deviceAssets = [deviceAsset1, deviceAsset3]; final deviceAssets = [deviceAsset1, deviceAsset3];
deviceAssets.sort((a, b) => a.createdAt.compareTo(b.createdAt)); deviceAssets.sort((a, b) => a.createdAt.compareTo(b.createdAt));
@ -804,7 +678,7 @@ void main() {
when(() => mockLocalAlbumRepo.getAssetsForAlbum(emptyDbAlbum.id)) when(() => mockLocalAlbumRepo.getAssetsForAlbum(emptyDbAlbum.id))
.thenAnswer((_) async => []); .thenAnswer((_) async => []);
final result = await sut.fullSync(emptyDbAlbum, refreshedWithAssets); final result = await sut.fullDiff(emptyDbAlbum, refreshedWithAssets);
expect(result, isTrue); expect(result, isTrue);
verify(() => mockAlbumMediaRepo.getAssetsForAlbum(emptyDbAlbum.id)) verify(() => mockAlbumMediaRepo.getAssetsForAlbum(emptyDbAlbum.id))
@ -816,13 +690,10 @@ void main() {
verify( verify(
() => mockLocalAlbumRepo.update( () => mockLocalAlbumRepo.update(
any( any(
that: predicate<LocalAlbum>( that: predicate<LocalAlbum>((a) =>
(a) => a.id == emptyDbAlbum.id &&
a.id == emptyDbAlbum.id && a.assetCount == deviceAssets.length &&
a.assetCount == deviceAssets.length && a.updatedAt == refreshedWithAssets.updatedAt),
a.updatedAt == refreshedWithAssets.updatedAt &&
a.thumbnailId == deviceAssets.first.id,
),
), ),
), ),
).called(1); ).called(1);
@ -844,7 +715,7 @@ void main() {
(_) async => dbAssets, (_) async => dbAssets,
); );
final result = await sut.fullSync(dbAlbum, currentRefreshedAlbum); final result = await sut.fullDiff(dbAlbum, currentRefreshedAlbum);
expect(result, isTrue); expect(result, isTrue);
verify(() => mockAlbumMediaRepo.getAssetsForAlbum(dbAlbum.id)).called(1); verify(() => mockAlbumMediaRepo.getAssetsForAlbum(dbAlbum.id)).called(1);
@ -871,13 +742,10 @@ void main() {
verify( verify(
() => mockLocalAlbumRepo.update( () => mockLocalAlbumRepo.update(
any( any(
that: predicate<LocalAlbum>( that: predicate<LocalAlbum>((a) =>
(a) => a.id == dbAlbum.id &&
a.id == dbAlbum.id && a.assetCount == 2 &&
a.assetCount == 2 && a.updatedAt == currentRefreshedAlbum.updatedAt),
a.updatedAt == currentRefreshedAlbum.updatedAt &&
a.thumbnailId == deviceAssets.first.id,
),
), ),
), ),
).called(1); ).called(1);
@ -901,7 +769,7 @@ void main() {
when(() => mockLocalAlbumRepo.getAssetsForAlbum(dbAlbum.id)) when(() => mockLocalAlbumRepo.getAssetsForAlbum(dbAlbum.id))
.thenAnswer((_) async => dbAssets); .thenAnswer((_) async => dbAssets);
final result = await sut.fullSync(dbAlbum, currentRefreshedAlbum); final result = await sut.fullDiff(dbAlbum, currentRefreshedAlbum);
expect(result, isTrue); expect(result, isTrue);
verify(() => mockAlbumMediaRepo.getAssetsForAlbum(dbAlbum.id)).called(1); verify(() => mockAlbumMediaRepo.getAssetsForAlbum(dbAlbum.id)).called(1);
@ -912,13 +780,10 @@ void main() {
verify( verify(
() => mockLocalAlbumRepo.update( () => mockLocalAlbumRepo.update(
any( any(
that: predicate<LocalAlbum>( that: predicate<LocalAlbum>((a) =>
(a) => a.id == dbAlbum.id &&
a.id == dbAlbum.id && a.assetCount == 2 &&
a.assetCount == 2 && a.updatedAt == currentRefreshedAlbum.updatedAt),
a.updatedAt == currentRefreshedAlbum.updatedAt &&
a.thumbnailId == deviceAssets.first.id,
),
), ),
), ),
).called(1); ).called(1);