mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
sync fixes
This commit is contained in:
parent
61316d94a9
commit
f36049a696
@ -2,12 +2,12 @@ package app.alextran.immich
|
|||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import app.alextran.immich.platform.MessagesImpl
|
import app.alextran.immich.platform.ImHostApiImpl
|
||||||
|
|
||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : FlutterActivity() {
|
||||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
flutterEngine.plugins.add(BackgroundServicePlugin())
|
flutterEngine.plugins.add(BackgroundServicePlugin())
|
||||||
ImHostService.setUp(flutterEngine.dartExecutor.binaryMessenger, MessagesImpl(this))
|
ImHostApi.setUp(flutterEngine.dartExecutor.binaryMessenger, ImHostApiImpl(this))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,6 +80,7 @@ class MediaManager(context: Context) {
|
|||||||
val currentVolumes = MediaStore.getExternalVolumeNames(ctx)
|
val currentVolumes = MediaStore.getExternalVolumeNames(ctx)
|
||||||
val changed = mutableListOf<PlatformAsset>()
|
val changed = mutableListOf<PlatformAsset>()
|
||||||
val deleted = mutableListOf<String>()
|
val deleted = mutableListOf<String>()
|
||||||
|
val albumAssets = mutableMapOf<String, List<String>>()
|
||||||
|
|
||||||
var hasChanges = genMap.keys != currentVolumes
|
var hasChanges = genMap.keys != currentVolumes
|
||||||
for (volume in currentVolumes) {
|
for (volume in currentVolumes) {
|
||||||
@ -160,15 +161,15 @@ class MediaManager(context: Context) {
|
|||||||
mediaType.toLong(),
|
mediaType.toLong(),
|
||||||
createdAt,
|
createdAt,
|
||||||
modifiedAt,
|
modifiedAt,
|
||||||
duration,
|
duration
|
||||||
listOf(bucketId)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
albumAssets.put(id, listOf(bucketId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Unmounted volumes are handled in dart when the album is removed
|
// Unmounted volumes are handled in dart when the album is removed
|
||||||
|
|
||||||
return SyncDelta(hasChanges, changed, deleted)
|
return SyncDelta(hasChanges, changed, deleted, albumAssets)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,8 +84,7 @@ data class PlatformAsset (
|
|||||||
val type: Long,
|
val type: Long,
|
||||||
val createdAt: Long? = null,
|
val createdAt: Long? = null,
|
||||||
val updatedAt: Long? = null,
|
val updatedAt: Long? = null,
|
||||||
val durationInSeconds: Long,
|
val durationInSeconds: Long
|
||||||
val albumIds: List<String>
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
companion object {
|
companion object {
|
||||||
@ -96,8 +95,7 @@ data class PlatformAsset (
|
|||||||
val createdAt = pigeonVar_list[3] as Long?
|
val createdAt = pigeonVar_list[3] as Long?
|
||||||
val updatedAt = pigeonVar_list[4] as Long?
|
val updatedAt = pigeonVar_list[4] as Long?
|
||||||
val durationInSeconds = pigeonVar_list[5] as Long
|
val durationInSeconds = pigeonVar_list[5] as Long
|
||||||
val albumIds = pigeonVar_list[6] as List<String>
|
return PlatformAsset(id, name, type, createdAt, updatedAt, durationInSeconds)
|
||||||
return PlatformAsset(id, name, type, createdAt, updatedAt, durationInSeconds, albumIds)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun toList(): List<Any?> {
|
fun toList(): List<Any?> {
|
||||||
@ -108,7 +106,6 @@ data class PlatformAsset (
|
|||||||
createdAt,
|
createdAt,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
durationInSeconds,
|
durationInSeconds,
|
||||||
albumIds,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
@ -127,7 +124,8 @@ data class PlatformAsset (
|
|||||||
data class SyncDelta (
|
data class SyncDelta (
|
||||||
val hasChanges: Boolean,
|
val hasChanges: Boolean,
|
||||||
val updates: List<PlatformAsset>,
|
val updates: List<PlatformAsset>,
|
||||||
val deletes: List<String>
|
val deletes: List<String>,
|
||||||
|
val albumAssets: Map<String, List<String>>
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
companion object {
|
companion object {
|
||||||
@ -135,7 +133,8 @@ data class SyncDelta (
|
|||||||
val hasChanges = pigeonVar_list[0] as Boolean
|
val hasChanges = pigeonVar_list[0] as Boolean
|
||||||
val updates = pigeonVar_list[1] as List<PlatformAsset>
|
val updates = pigeonVar_list[1] as List<PlatformAsset>
|
||||||
val deletes = pigeonVar_list[2] as List<String>
|
val deletes = pigeonVar_list[2] as List<String>
|
||||||
return SyncDelta(hasChanges, updates, deletes)
|
val albumAssets = pigeonVar_list[3] as Map<String, List<String>>
|
||||||
|
return SyncDelta(hasChanges, updates, deletes, albumAssets)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun toList(): List<Any?> {
|
fun toList(): List<Any?> {
|
||||||
@ -143,6 +142,7 @@ data class SyncDelta (
|
|||||||
hasChanges,
|
hasChanges,
|
||||||
updates,
|
updates,
|
||||||
deletes,
|
deletes,
|
||||||
|
albumAssets,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
@ -188,7 +188,7 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
interface ImHostService {
|
interface ImHostApi {
|
||||||
fun shouldFullSync(): Boolean
|
fun shouldFullSync(): Boolean
|
||||||
fun getMediaChanges(): SyncDelta
|
fun getMediaChanges(): SyncDelta
|
||||||
fun checkpointSync()
|
fun checkpointSync()
|
||||||
@ -196,17 +196,17 @@ interface ImHostService {
|
|||||||
fun getAssetIdsForAlbum(albumId: String): List<String>
|
fun getAssetIdsForAlbum(albumId: String): List<String>
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** The codec used by ImHostService. */
|
/** The codec used by ImHostApi. */
|
||||||
val codec: MessageCodec<Any?> by lazy {
|
val codec: MessageCodec<Any?> by lazy {
|
||||||
MessagesPigeonCodec()
|
MessagesPigeonCodec()
|
||||||
}
|
}
|
||||||
/** Sets up an instance of `ImHostService` to handle messages through the `binaryMessenger`. */
|
/** Sets up an instance of `ImHostApi` to handle messages through the `binaryMessenger`. */
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun setUp(binaryMessenger: BinaryMessenger, api: ImHostService?, messageChannelSuffix: String = "") {
|
fun setUp(binaryMessenger: BinaryMessenger, api: ImHostApi?, messageChannelSuffix: String = "") {
|
||||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
val taskQueue = binaryMessenger.makeBackgroundTaskQueue()
|
val taskQueue = binaryMessenger.makeBackgroundTaskQueue()
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostService.shouldFullSync$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostApi.shouldFullSync$separatedMessageChannelSuffix", codec)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
channel.setMessageHandler { _, reply ->
|
channel.setMessageHandler { _, reply ->
|
||||||
val wrapped: List<Any?> = try {
|
val wrapped: List<Any?> = try {
|
||||||
@ -221,7 +221,7 @@ interface ImHostService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostService.getMediaChanges$separatedMessageChannelSuffix", codec, taskQueue)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostApi.getMediaChanges$separatedMessageChannelSuffix", codec, taskQueue)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
channel.setMessageHandler { _, reply ->
|
channel.setMessageHandler { _, reply ->
|
||||||
val wrapped: List<Any?> = try {
|
val wrapped: List<Any?> = try {
|
||||||
@ -236,7 +236,7 @@ interface ImHostService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostService.checkpointSync$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostApi.checkpointSync$separatedMessageChannelSuffix", codec)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
channel.setMessageHandler { _, reply ->
|
channel.setMessageHandler { _, reply ->
|
||||||
val wrapped: List<Any?> = try {
|
val wrapped: List<Any?> = try {
|
||||||
@ -252,7 +252,7 @@ interface ImHostService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostService.clearSyncCheckpoint$separatedMessageChannelSuffix", codec)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostApi.clearSyncCheckpoint$separatedMessageChannelSuffix", codec)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
channel.setMessageHandler { _, reply ->
|
channel.setMessageHandler { _, reply ->
|
||||||
val wrapped: List<Any?> = try {
|
val wrapped: List<Any?> = try {
|
||||||
@ -268,7 +268,7 @@ interface ImHostService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostService.getAssetIdsForAlbum$separatedMessageChannelSuffix", codec, taskQueue)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ImHostApi.getAssetIdsForAlbum$separatedMessageChannelSuffix", codec, taskQueue)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
channel.setMessageHandler { message, reply ->
|
channel.setMessageHandler { message, reply ->
|
||||||
val args = message as List<Any?>
|
val args = message as List<Any?>
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
package app.alextran.immich.platform
|
package app.alextran.immich.platform
|
||||||
|
|
||||||
import ImHostService
|
import ImHostApi
|
||||||
import SyncDelta
|
import SyncDelta
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.ext.SdkExtensions
|
import android.os.ext.SdkExtensions
|
||||||
|
|
||||||
class MessagesImpl(context: Context) : ImHostService {
|
class ImHostApiImpl(context: Context) : ImHostApi {
|
||||||
private val ctx: Context = context.applicationContext
|
private val ctx: Context = context.applicationContext
|
||||||
private val mediaManager: MediaManager = MediaManager(ctx)
|
private val mediaManager: MediaManager = MediaManager(ctx)
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ import UIKit
|
|||||||
|
|
||||||
// Register pigeon handler
|
// Register pigeon handler
|
||||||
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
|
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
|
||||||
ImHostServiceSetup.setUp(binaryMessenger: controller.binaryMessenger, api: ImHostServiceImpl())
|
ImHostApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: ImHostApiImpl())
|
||||||
|
|
||||||
BackgroundServicePlugin.setPluginRegistrantCallback { registry in
|
BackgroundServicePlugin.setPluginRegistrantCallback { registry in
|
||||||
if !registry.hasPlugin("org.cocoapods.path-provider-foundation") {
|
if !registry.hasPlugin("org.cocoapods.path-provider-foundation") {
|
||||||
|
@ -97,7 +97,7 @@ class MediaManager {
|
|||||||
|
|
||||||
let currentToken = PHPhotoLibrary.shared().currentChangeToken
|
let currentToken = PHPhotoLibrary.shared().currentChangeToken
|
||||||
if storedToken == currentToken {
|
if storedToken == currentToken {
|
||||||
return SyncDelta(hasChanges: false, updates: [], deletes: [])
|
return SyncDelta(hasChanges: false, updates: [], deletes: [], albumAssets: [:])
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
@ -110,6 +110,7 @@ class MediaManager {
|
|||||||
guard let details = try? change.changeDetails(for: PHObjectType.asset) else { continue }
|
guard let details = try? change.changeDetails(for: PHObjectType.asset) else { continue }
|
||||||
|
|
||||||
let updated = details.updatedLocalIdentifiers.union(details.insertedLocalIdentifiers)
|
let updated = details.updatedLocalIdentifiers.union(details.insertedLocalIdentifiers)
|
||||||
|
deletedAssets.formUnion(details.deletedLocalIdentifiers)
|
||||||
if (updated.isEmpty) { continue }
|
if (updated.isEmpty) { continue }
|
||||||
|
|
||||||
let result = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: nil)
|
let result = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: nil)
|
||||||
@ -117,7 +118,7 @@ class MediaManager {
|
|||||||
let asset = result.object(at: i)
|
let asset = result.object(at: i)
|
||||||
|
|
||||||
// Asset wrapper only uses the id for comparison. Multiple change can contain the same asset, skip duplicate changes
|
// Asset wrapper only uses the id for comparison. Multiple change can contain the same asset, skip duplicate changes
|
||||||
let predicate = PlatformAsset(id: asset.localIdentifier, name: "", type: 0, createdAt: nil, updatedAt: nil, durationInSeconds: 0, albumIds: [])
|
let predicate = PlatformAsset(id: asset.localIdentifier, name: "", type: 0, createdAt: nil, updatedAt: nil, durationInSeconds: 0)
|
||||||
if (updatedAssets.contains(AssetWrapper(with: predicate))) {
|
if (updatedAssets.contains(AssetWrapper(with: predicate))) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -136,37 +137,33 @@ class MediaManager {
|
|||||||
createdAt: createdAt.map { Int64($0) },
|
createdAt: createdAt.map { Int64($0) },
|
||||||
updatedAt: updatedAt.map { Int64($0) },
|
updatedAt: updatedAt.map { Int64($0) },
|
||||||
durationInSeconds: durationInSeconds,
|
durationInSeconds: durationInSeconds,
|
||||||
albumIds: self._getAlbumIds(forAsset: asset)
|
|
||||||
))
|
))
|
||||||
|
|
||||||
updatedAssets.insert(domainAsset)
|
updatedAssets.insert(domainAsset)
|
||||||
}
|
}
|
||||||
|
|
||||||
deletedAssets.formUnion(details.deletedLocalIdentifiers)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return SyncDelta(hasChanges: true, updates: Array(updatedAssets.map { $0.asset }), deletes: Array(deletedAssets))
|
let updates = Array(updatedAssets.map { $0.asset })
|
||||||
|
return SyncDelta(hasChanges: true, updates: updates, deletes: Array(deletedAssets), albumAssets: buildAlbumAssetsMap(assets: updates))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 16, *)
|
private func buildAlbumAssetsMap(assets: Array<PlatformAsset>) -> [String: [String]] {
|
||||||
func _getAlbumIds(forAsset: PHAsset) -> [String] {
|
var albumAssets: [String: [String]] = [:]
|
||||||
var albumIds: [String] = []
|
|
||||||
let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum]
|
let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum]
|
||||||
|
|
||||||
albumTypes.forEach { type in
|
albumTypes.forEach { type in
|
||||||
let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil)
|
let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil)
|
||||||
collections.enumerateObjects { (album, _, _) in
|
collections.enumerateObjects { (album, _, _) in
|
||||||
var options = PHFetchOptions()
|
let options = PHFetchOptions()
|
||||||
options.fetchLimit = 1
|
options.predicate = NSPredicate(format: "localIdentifier IN %@", assets.map(\.id))
|
||||||
options.predicate = NSPredicate(format: "localIdentifier == %@", forAsset.localIdentifier)
|
|
||||||
let result = PHAsset.fetchAssets(in: album, options: options)
|
let result = PHAsset.fetchAssets(in: album, options: options)
|
||||||
if(result.count == 1) {
|
for i in 0..<result.count {
|
||||||
albumIds.append(album.localIdentifier)
|
let asset = result.object(at: i)
|
||||||
|
albumAssets[asset.localIdentifier, default: []].append(album.localIdentifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return albumIds
|
return albumAssets
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -136,7 +136,6 @@ struct PlatformAsset: Hashable {
|
|||||||
var createdAt: Int64? = nil
|
var createdAt: Int64? = nil
|
||||||
var updatedAt: Int64? = nil
|
var updatedAt: Int64? = nil
|
||||||
var durationInSeconds: Int64
|
var durationInSeconds: Int64
|
||||||
var albumIds: [String]
|
|
||||||
|
|
||||||
|
|
||||||
// swift-format-ignore: AlwaysUseLowerCamelCase
|
// swift-format-ignore: AlwaysUseLowerCamelCase
|
||||||
@ -147,7 +146,6 @@ struct PlatformAsset: Hashable {
|
|||||||
let createdAt: Int64? = nilOrValue(pigeonVar_list[3])
|
let createdAt: Int64? = nilOrValue(pigeonVar_list[3])
|
||||||
let updatedAt: Int64? = nilOrValue(pigeonVar_list[4])
|
let updatedAt: Int64? = nilOrValue(pigeonVar_list[4])
|
||||||
let durationInSeconds = pigeonVar_list[5] as! Int64
|
let durationInSeconds = pigeonVar_list[5] as! Int64
|
||||||
let albumIds = pigeonVar_list[6] as! [String]
|
|
||||||
|
|
||||||
return PlatformAsset(
|
return PlatformAsset(
|
||||||
id: id,
|
id: id,
|
||||||
@ -155,8 +153,7 @@ struct PlatformAsset: Hashable {
|
|||||||
type: type,
|
type: type,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
updatedAt: updatedAt,
|
updatedAt: updatedAt,
|
||||||
durationInSeconds: durationInSeconds,
|
durationInSeconds: durationInSeconds
|
||||||
albumIds: albumIds
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
func toList() -> [Any?] {
|
func toList() -> [Any?] {
|
||||||
@ -167,7 +164,6 @@ struct PlatformAsset: Hashable {
|
|||||||
createdAt,
|
createdAt,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
durationInSeconds,
|
durationInSeconds,
|
||||||
albumIds,
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
|
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
|
||||||
@ -182,6 +178,7 @@ struct SyncDelta: Hashable {
|
|||||||
var hasChanges: Bool
|
var hasChanges: Bool
|
||||||
var updates: [PlatformAsset]
|
var updates: [PlatformAsset]
|
||||||
var deletes: [String]
|
var deletes: [String]
|
||||||
|
var albumAssets: [String: [String]]
|
||||||
|
|
||||||
|
|
||||||
// swift-format-ignore: AlwaysUseLowerCamelCase
|
// swift-format-ignore: AlwaysUseLowerCamelCase
|
||||||
@ -189,11 +186,13 @@ struct SyncDelta: Hashable {
|
|||||||
let hasChanges = pigeonVar_list[0] as! Bool
|
let hasChanges = pigeonVar_list[0] as! Bool
|
||||||
let updates = pigeonVar_list[1] as! [PlatformAsset]
|
let updates = pigeonVar_list[1] as! [PlatformAsset]
|
||||||
let deletes = pigeonVar_list[2] as! [String]
|
let deletes = pigeonVar_list[2] as! [String]
|
||||||
|
let albumAssets = pigeonVar_list[3] as! [String: [String]]
|
||||||
|
|
||||||
return SyncDelta(
|
return SyncDelta(
|
||||||
hasChanges: hasChanges,
|
hasChanges: hasChanges,
|
||||||
updates: updates,
|
updates: updates,
|
||||||
deletes: deletes
|
deletes: deletes,
|
||||||
|
albumAssets: albumAssets
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
func toList() -> [Any?] {
|
func toList() -> [Any?] {
|
||||||
@ -201,6 +200,7 @@ struct SyncDelta: Hashable {
|
|||||||
hasChanges,
|
hasChanges,
|
||||||
updates,
|
updates,
|
||||||
deletes,
|
deletes,
|
||||||
|
albumAssets,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
static func == (lhs: SyncDelta, rhs: SyncDelta) -> Bool {
|
static func == (lhs: SyncDelta, rhs: SyncDelta) -> Bool {
|
||||||
@ -252,7 +252,7 @@ class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||||
protocol ImHostService {
|
protocol ImHostApi {
|
||||||
func shouldFullSync() throws -> Bool
|
func shouldFullSync() throws -> Bool
|
||||||
func getMediaChanges() throws -> SyncDelta
|
func getMediaChanges() throws -> SyncDelta
|
||||||
func checkpointSync() throws
|
func checkpointSync() throws
|
||||||
@ -261,17 +261,17 @@ protocol ImHostService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||||
class ImHostServiceSetup {
|
class ImHostApiSetup {
|
||||||
static var codec: FlutterStandardMessageCodec { MessagesPigeonCodec.shared }
|
static var codec: FlutterStandardMessageCodec { MessagesPigeonCodec.shared }
|
||||||
/// Sets up an instance of `ImHostService` to handle messages through the `binaryMessenger`.
|
/// Sets up an instance of `ImHostApi` to handle messages through the `binaryMessenger`.
|
||||||
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: ImHostService?, messageChannelSuffix: String = "") {
|
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: ImHostApi?, messageChannelSuffix: String = "") {
|
||||||
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
let taskQueue = binaryMessenger.makeBackgroundTaskQueue?()
|
let taskQueue = binaryMessenger.makeBackgroundTaskQueue?()
|
||||||
#else
|
#else
|
||||||
let taskQueue: FlutterTaskQueue? = nil
|
let taskQueue: FlutterTaskQueue? = nil
|
||||||
#endif
|
#endif
|
||||||
let shouldFullSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostService.shouldFullSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
let shouldFullSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostApi.shouldFullSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
if let api = api {
|
if let api = api {
|
||||||
shouldFullSyncChannel.setMessageHandler { _, reply in
|
shouldFullSyncChannel.setMessageHandler { _, reply in
|
||||||
do {
|
do {
|
||||||
@ -285,8 +285,8 @@ class ImHostServiceSetup {
|
|||||||
shouldFullSyncChannel.setMessageHandler(nil)
|
shouldFullSyncChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
let getMediaChangesChannel = taskQueue == 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.ImHostApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostService.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
||||||
if let api = api {
|
if let api = api {
|
||||||
getMediaChangesChannel.setMessageHandler { _, reply in
|
getMediaChangesChannel.setMessageHandler { _, reply in
|
||||||
do {
|
do {
|
||||||
@ -299,7 +299,7 @@ class ImHostServiceSetup {
|
|||||||
} else {
|
} else {
|
||||||
getMediaChangesChannel.setMessageHandler(nil)
|
getMediaChangesChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
let checkpointSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostService.checkpointSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
let checkpointSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostApi.checkpointSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
if let api = api {
|
if let api = api {
|
||||||
checkpointSyncChannel.setMessageHandler { _, reply in
|
checkpointSyncChannel.setMessageHandler { _, reply in
|
||||||
do {
|
do {
|
||||||
@ -312,7 +312,7 @@ class ImHostServiceSetup {
|
|||||||
} else {
|
} else {
|
||||||
checkpointSyncChannel.setMessageHandler(nil)
|
checkpointSyncChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
let clearSyncCheckpointChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostService.clearSyncCheckpoint\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
let clearSyncCheckpointChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostApi.clearSyncCheckpoint\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
if let api = api {
|
if let api = api {
|
||||||
clearSyncCheckpointChannel.setMessageHandler { _, reply in
|
clearSyncCheckpointChannel.setMessageHandler { _, reply in
|
||||||
do {
|
do {
|
||||||
@ -326,8 +326,8 @@ class ImHostServiceSetup {
|
|||||||
clearSyncCheckpointChannel.setMessageHandler(nil)
|
clearSyncCheckpointChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
let getAssetIdsForAlbumChannel = taskQueue == nil
|
let getAssetIdsForAlbumChannel = taskQueue == nil
|
||||||
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostService.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostApi.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostService.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ImHostApi.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
||||||
if let api = api {
|
if let api = api {
|
||||||
getAssetIdsForAlbumChannel.setMessageHandler { message, reply in
|
getAssetIdsForAlbumChannel.setMessageHandler { message, reply in
|
||||||
let args = message as! [Any?]
|
let args = message as! [Any?]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import Photos
|
import Photos
|
||||||
|
|
||||||
class ImHostServiceImpl: ImHostService {
|
class ImHostApiImpl: ImHostApi {
|
||||||
|
|
||||||
private let mediaManager: MediaManager
|
private let mediaManager: MediaManager
|
||||||
|
|
||||||
|
@ -2,18 +2,12 @@ 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';
|
||||||
|
|
||||||
abstract interface class IAlbumMediaRepository {
|
abstract interface class IAlbumMediaRepository {
|
||||||
Future<List<LocalAlbum>> getAll();
|
Future<List<LocalAlbum>> getAll({bool withModifiedTime = false});
|
||||||
|
|
||||||
Future<List<LocalAsset>> getAssetsForAlbum(
|
Future<List<LocalAsset>> getAssetsForAlbum(
|
||||||
String albumId, {
|
String albumId, {
|
||||||
DateTimeFilter? updateTimeCond,
|
DateTimeFilter? updateTimeCond,
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<LocalAlbum> refresh(
|
|
||||||
String albumId, {
|
|
||||||
bool withModifiedTime = true,
|
|
||||||
bool withAssetCount = true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class DateTimeFilter {
|
class DateTimeFilter {
|
||||||
|
@ -15,60 +15,59 @@ class DeviceSyncService {
|
|||||||
final IAlbumMediaRepository _albumMediaRepository;
|
final IAlbumMediaRepository _albumMediaRepository;
|
||||||
final ILocalAlbumRepository _localAlbumRepository;
|
final ILocalAlbumRepository _localAlbumRepository;
|
||||||
final Platform _platform;
|
final Platform _platform;
|
||||||
final platform.ImHostService _hostService;
|
final platform.ImHostApi _hostApi;
|
||||||
final Logger _log = Logger("DeviceSyncService");
|
final Logger _log = Logger("DeviceSyncService");
|
||||||
|
|
||||||
DeviceSyncService({
|
DeviceSyncService({
|
||||||
required IAlbumMediaRepository albumMediaRepository,
|
required IAlbumMediaRepository albumMediaRepository,
|
||||||
required ILocalAlbumRepository localAlbumRepository,
|
required ILocalAlbumRepository localAlbumRepository,
|
||||||
required platform.ImHostService hostService,
|
required platform.ImHostApi hostApi,
|
||||||
Platform? platform,
|
Platform? platform,
|
||||||
}) : _albumMediaRepository = albumMediaRepository,
|
}) : _albumMediaRepository = albumMediaRepository,
|
||||||
_localAlbumRepository = localAlbumRepository,
|
_localAlbumRepository = localAlbumRepository,
|
||||||
_platform = platform ?? const LocalPlatform(),
|
_platform = platform ?? const LocalPlatform(),
|
||||||
_hostService = hostService;
|
_hostApi = hostApi;
|
||||||
|
|
||||||
Future<void> sync() async {
|
Future<void> sync() async {
|
||||||
final Stopwatch stopwatch = Stopwatch()..start();
|
final Stopwatch stopwatch = Stopwatch()..start();
|
||||||
try {
|
try {
|
||||||
if (await _hostService.shouldFullSync()) {
|
if (await _hostApi.shouldFullSync()) {
|
||||||
_log.fine("Cannot use partial sync. Performing full sync");
|
_log.fine("Cannot use partial sync. Performing full sync");
|
||||||
return await fullSync();
|
return await fullSync();
|
||||||
}
|
}
|
||||||
|
|
||||||
final delta = await _hostService.getMediaChanges();
|
final delta = await _hostApi.getMediaChanges();
|
||||||
if (!delta.hasChanges) {
|
if (!delta.hasChanges) {
|
||||||
_log.fine("No media changes detected. Skipping sync");
|
_log.fine("No media changes detected. Skipping sync");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final deviceAlbums = await _albumMediaRepository.getAll();
|
final deviceAlbums =
|
||||||
|
await _albumMediaRepository.getAll(withModifiedTime: true);
|
||||||
await _localAlbumRepository.updateAll(deviceAlbums);
|
await _localAlbumRepository.updateAll(deviceAlbums);
|
||||||
await _localAlbumRepository.processDelta(delta);
|
await _localAlbumRepository.processDelta(delta);
|
||||||
|
|
||||||
if (_platform.isAndroid) {
|
if (_platform.isAndroid) {
|
||||||
final dbAlbums = await _localAlbumRepository.getAll();
|
final dbAlbums = await _localAlbumRepository.getAll();
|
||||||
for (final album in dbAlbums) {
|
for (final album in dbAlbums) {
|
||||||
final deviceIds = await _hostService.getAssetIdsForAlbum(album.id);
|
final deviceIds = await _hostApi.getAssetIdsForAlbum(album.id);
|
||||||
await _localAlbumRepository.syncAlbumDeletes(album.id, deviceIds);
|
await _localAlbumRepository.syncAlbumDeletes(album.id, deviceIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await _hostService.checkpointSync();
|
await _hostApi.checkpointSync();
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
_log.severe("Error performing device sync", e, s);
|
_log.severe("Error performing device sync", e, s);
|
||||||
|
} finally {
|
||||||
|
stopwatch.stop();
|
||||||
|
_log.info("Device sync took - ${stopwatch.elapsedMilliseconds}ms");
|
||||||
}
|
}
|
||||||
stopwatch.stop();
|
|
||||||
_log.info("Device sync took - ${stopwatch.elapsedMilliseconds}ms");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> fullSync() async {
|
Future<void> fullSync() async {
|
||||||
try {
|
try {
|
||||||
final Stopwatch stopwatch = Stopwatch()..start();
|
final Stopwatch stopwatch = Stopwatch()..start();
|
||||||
// The deviceAlbums will not have the updatedAt field
|
|
||||||
// and the assetCount will be 0. They are refreshed later
|
|
||||||
// after the comparison. The orderby in the filter sorts the assets
|
|
||||||
// and not the albums.
|
|
||||||
final deviceAlbums =
|
final deviceAlbums =
|
||||||
(await _albumMediaRepository.getAll()).sortedBy((a) => a.id);
|
(await _albumMediaRepository.getAll()).sortedBy((a) => a.id);
|
||||||
|
|
||||||
@ -84,7 +83,7 @@ class DeviceSyncService {
|
|||||||
onlySecond: addAlbum,
|
onlySecond: addAlbum,
|
||||||
);
|
);
|
||||||
|
|
||||||
await _hostService.checkpointSync();
|
await _hostApi.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) {
|
||||||
@ -92,10 +91,9 @@ class DeviceSyncService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addAlbum(LocalAlbum newAlbum) async {
|
Future<void> addAlbum(LocalAlbum album) async {
|
||||||
try {
|
try {
|
||||||
_log.fine("Adding device album ${newAlbum.name}");
|
_log.fine("Adding device album ${album.name}");
|
||||||
final album = await _albumMediaRepository.refresh(newAlbum.id);
|
|
||||||
|
|
||||||
final assets = album.assetCount > 0
|
final assets = album.assetCount > 0
|
||||||
? await _albumMediaRepository.getAssetsForAlbum(album.id)
|
? await _albumMediaRepository.getAssetsForAlbum(album.id)
|
||||||
@ -119,15 +117,11 @@ class DeviceSyncService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The deviceAlbum is ignored since we are going to refresh it anyways
|
// The deviceAlbum is ignored since we are going to refresh it anyways
|
||||||
FutureOr<bool> updateAlbum(LocalAlbum dbAlbum, LocalAlbum _) async {
|
FutureOr<bool> updateAlbum(LocalAlbum dbAlbum, LocalAlbum deviceAlbum) async {
|
||||||
try {
|
try {
|
||||||
_log.fine("Syncing device album ${dbAlbum.name}");
|
_log.fine("Syncing device album ${dbAlbum.name}");
|
||||||
|
|
||||||
final deviceAlbum = await _albumMediaRepository.refresh(dbAlbum.id);
|
if (_albumsEqual(deviceAlbum, dbAlbum)) {
|
||||||
|
|
||||||
// Early return if album hasn't changed
|
|
||||||
if (deviceAlbum.updatedAt.isAtSameMomentAs(dbAlbum.updatedAt) &&
|
|
||||||
deviceAlbum.assetCount == dbAlbum.assetCount) {
|
|
||||||
_log.fine(
|
_log.fine(
|
||||||
"Device album ${dbAlbum.name} has not changed. Skipping sync.",
|
"Device album ${dbAlbum.name} has not changed. Skipping sync.",
|
||||||
);
|
);
|
||||||
@ -293,4 +287,10 @@ class DeviceSyncService {
|
|||||||
a.height == b.height &&
|
a.height == b.height &&
|
||||||
a.durationInSeconds == b.durationInSeconds;
|
a.durationInSeconds == b.durationInSeconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _albumsEqual(LocalAlbum a, LocalAlbum b) {
|
||||||
|
return a.name == b.name &&
|
||||||
|
a.assetCount == b.assetCount &&
|
||||||
|
a.updatedAt.isAtSameMomentAs(b.updatedAt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,24 +11,20 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
|
|||||||
const AlbumMediaRepository({Platform platform = const LocalPlatform()})
|
const AlbumMediaRepository({Platform platform = const LocalPlatform()})
|
||||||
: _platform = platform;
|
: _platform = platform;
|
||||||
|
|
||||||
PMFilter _getAlbumFilter({
|
PMFilter _getAlbumFilter({DateTimeFilter? updateTimeCond}) =>
|
||||||
withAssetTitle = false,
|
|
||||||
withModifiedTime = false,
|
|
||||||
DateTimeFilter? updateTimeCond,
|
|
||||||
}) =>
|
|
||||||
FilterOptionGroup(
|
FilterOptionGroup(
|
||||||
imageOption: FilterOption(
|
imageOption: const FilterOption(
|
||||||
// needTitle is expected to be slow on iOS but is required to fetch the asset title
|
// needTitle is expected to be slow on iOS but is required to fetch the asset title
|
||||||
needTitle: withAssetTitle,
|
needTitle: true,
|
||||||
sizeConstraint: const SizeConstraint(ignoreSize: true),
|
sizeConstraint: SizeConstraint(ignoreSize: true),
|
||||||
),
|
),
|
||||||
videoOption: FilterOption(
|
videoOption: const FilterOption(
|
||||||
needTitle: withAssetTitle,
|
needTitle: true,
|
||||||
sizeConstraint: const SizeConstraint(ignoreSize: true),
|
sizeConstraint: SizeConstraint(ignoreSize: true),
|
||||||
durationConstraint: const DurationConstraint(allowNullable: true),
|
durationConstraint: DurationConstraint(allowNullable: true),
|
||||||
),
|
),
|
||||||
// This is needed to get the modified time of the album
|
// This is needed to get the modified time of the album
|
||||||
containsPathModified: withModifiedTime,
|
containsPathModified: true,
|
||||||
createTimeCond: DateTimeCond.def().copyWith(ignore: true),
|
createTimeCond: DateTimeCond.def().copyWith(ignore: true),
|
||||||
updateTimeCond: updateTimeCond == null
|
updateTimeCond: updateTimeCond == null
|
||||||
? DateTimeCond.def().copyWith(ignore: true)
|
? DateTimeCond.def().copyWith(ignore: true)
|
||||||
@ -40,10 +36,10 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
|
|||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<LocalAlbum>> getAll() {
|
Future<List<LocalAlbum>> getAll({bool withModifiedTime = false}) {
|
||||||
return PhotoManager.getAssetPathList(
|
return PhotoManager.getAssetPathList(
|
||||||
hasAll: true,
|
hasAll: true,
|
||||||
filterOption: AdvancedCustomFilter(),
|
filterOption: _getAlbumFilter(),
|
||||||
).then((e) {
|
).then((e) {
|
||||||
if (_platform.isAndroid) {
|
if (_platform.isAndroid) {
|
||||||
e.removeWhere((a) => a.isAll);
|
e.removeWhere((a) => a.isAll);
|
||||||
@ -59,10 +55,7 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
|
|||||||
}) async {
|
}) async {
|
||||||
final assetPathEntity = await AssetPathEntity.obtainPathFromProperties(
|
final assetPathEntity = await AssetPathEntity.obtainPathFromProperties(
|
||||||
id: albumId,
|
id: albumId,
|
||||||
optionGroup: _getAlbumFilter(
|
optionGroup: _getAlbumFilter(updateTimeCond: updateTimeCond),
|
||||||
withAssetTitle: true,
|
|
||||||
updateTimeCond: updateTimeCond,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
final assets = <AssetEntity>[];
|
final assets = <AssetEntity>[];
|
||||||
int pageNumber = 0, lastPageCount = 0;
|
int pageNumber = 0, lastPageCount = 0;
|
||||||
@ -77,17 +70,6 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
|
|||||||
} while (lastPageCount == kFetchLocalAssetsBatchSize);
|
} while (lastPageCount == kFetchLocalAssetsBatchSize);
|
||||||
return Future.wait(assets.map((a) => a.toDto()));
|
return Future.wait(assets.map((a) => a.toDto()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<LocalAlbum> refresh(
|
|
||||||
String albumId, {
|
|
||||||
bool withModifiedTime = true,
|
|
||||||
bool withAssetCount = true,
|
|
||||||
}) =>
|
|
||||||
AssetPathEntity.obtainPathFromProperties(
|
|
||||||
id: albumId,
|
|
||||||
optionGroup: _getAlbumFilter(withModifiedTime: withModifiedTime),
|
|
||||||
).then((a) => a.toDto(withAssetCount: withAssetCount));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on AssetEntity {
|
extension on AssetEntity {
|
||||||
|
@ -171,7 +171,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
|
..where(_db.localAlbumAssetEntity.albumId.equals(albumId))
|
||||||
..orderBy([OrderingTerm.desc(_db.localAssetEntity.id)]);
|
..orderBy([OrderingTerm.asc(_db.localAssetEntity.id)]);
|
||||||
return query
|
return query
|
||||||
.map((row) => row.readTable(_db.localAssetEntity).toDto())
|
.map((row) => row.readTable(_db.localAssetEntity).toDto())
|
||||||
.get();
|
.get();
|
||||||
@ -193,25 +193,37 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository
|
|||||||
await _deleteAssets(delta.deletes);
|
await _deleteAssets(delta.deletes);
|
||||||
|
|
||||||
await _upsertAssets(delta.updates.map((a) => a.toLocalAsset()));
|
await _upsertAssets(delta.updates.map((a) => a.toLocalAsset()));
|
||||||
|
// The ugly casting below is required for now because the generated code
|
||||||
|
// casts the returned values from the platform during decoding them
|
||||||
|
// and iterating over them causes the type to be List<Object?> instead of
|
||||||
|
// List<String>
|
||||||
await _db.batch((batch) async {
|
await _db.batch((batch) async {
|
||||||
for (final asset in delta.updates) {
|
delta.albumAssets
|
||||||
|
.cast<String, List<Object?>>()
|
||||||
|
.forEach((assetId, albumIds) {
|
||||||
batch.deleteWhere(
|
batch.deleteWhere(
|
||||||
_db.localAlbumAssetEntity,
|
_db.localAlbumAssetEntity,
|
||||||
(f) =>
|
(f) =>
|
||||||
f.albumId.isNotIn(asset.albumIds) & f.assetId.equals(asset.id),
|
f.albumId.isNotIn(albumIds.cast<String?>().nonNulls) &
|
||||||
|
f.assetId.equals(assetId),
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await _db.batch((batch) async {
|
||||||
|
delta.albumAssets
|
||||||
|
.cast<String, List<Object?>>()
|
||||||
|
.forEach((assetId, albumIds) {
|
||||||
batch.insertAll(
|
batch.insertAll(
|
||||||
_db.localAlbumAssetEntity,
|
_db.localAlbumAssetEntity,
|
||||||
asset.albumIds.map(
|
albumIds.cast<String?>().nonNulls.map(
|
||||||
(albumId) => LocalAlbumAssetEntityCompanion.insert(
|
(albumId) => LocalAlbumAssetEntityCompanion.insert(
|
||||||
assetId: asset.id,
|
assetId: assetId,
|
||||||
albumId: albumId,
|
albumId: albumId,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onConflict: DoNothing(),
|
onConflict: DoNothing(),
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,6 @@ class PlatformAsset {
|
|||||||
final int? createdAt;
|
final int? createdAt;
|
||||||
final int? updatedAt;
|
final int? updatedAt;
|
||||||
final int durationInSeconds;
|
final int durationInSeconds;
|
||||||
final List<String> albumIds;
|
|
||||||
|
|
||||||
const PlatformAsset({
|
const PlatformAsset({
|
||||||
required this.id,
|
required this.id,
|
||||||
@ -29,23 +28,26 @@ class PlatformAsset {
|
|||||||
this.createdAt,
|
this.createdAt,
|
||||||
this.updatedAt,
|
this.updatedAt,
|
||||||
this.durationInSeconds = 0,
|
this.durationInSeconds = 0,
|
||||||
this.albumIds = const [],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class SyncDelta {
|
class SyncDelta {
|
||||||
SyncDelta({
|
final bool hasChanges;
|
||||||
|
final List<PlatformAsset> updates;
|
||||||
|
final List<String> deletes;
|
||||||
|
// Asset -> Album mapping
|
||||||
|
final Map<String, List<String>> albumAssets;
|
||||||
|
|
||||||
|
const SyncDelta({
|
||||||
this.hasChanges = false,
|
this.hasChanges = false,
|
||||||
this.updates = const [],
|
this.updates = const [],
|
||||||
this.deletes = const [],
|
this.deletes = const [],
|
||||||
|
this.albumAssets = const {},
|
||||||
});
|
});
|
||||||
bool hasChanges;
|
|
||||||
List<PlatformAsset> updates;
|
|
||||||
List<String> deletes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostApi()
|
@HostApi()
|
||||||
abstract class ImHostService {
|
abstract class ImHostApi {
|
||||||
bool shouldFullSync();
|
bool shouldFullSync();
|
||||||
|
|
||||||
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
|
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
|
||||||
|
91
mobile/lib/platform/messages.g.dart
generated
91
mobile/lib/platform/messages.g.dart
generated
@ -14,22 +14,21 @@ PlatformException _createConnectionError(String channelName) {
|
|||||||
message: 'Unable to establish connection on channel: "$channelName".',
|
message: 'Unable to establish connection on channel: "$channelName".',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _deepEquals(Object? a, Object? b) {
|
bool _deepEquals(Object? a, Object? b) {
|
||||||
if (a is List && b is List) {
|
if (a is List && b is List) {
|
||||||
return a.length == b.length &&
|
return a.length == b.length &&
|
||||||
a.indexed
|
a.indexed
|
||||||
.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
|
.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
|
||||||
}
|
}
|
||||||
if (a is Map && b is Map) {
|
if (a is Map && b is Map) {
|
||||||
return a.length == b.length &&
|
return a.length == b.length && a.entries.every((MapEntry<Object?, Object?> entry) =>
|
||||||
a.entries.every((MapEntry<Object?, Object?> entry) =>
|
(b as Map<Object?, Object?>).containsKey(entry.key) &&
|
||||||
(b as Map<Object?, Object?>).containsKey(entry.key) &&
|
_deepEquals(entry.value, b[entry.key]));
|
||||||
_deepEquals(entry.value, b[entry.key]));
|
|
||||||
}
|
}
|
||||||
return a == b;
|
return a == b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PlatformAsset {
|
class PlatformAsset {
|
||||||
PlatformAsset({
|
PlatformAsset({
|
||||||
required this.id,
|
required this.id,
|
||||||
@ -38,7 +37,6 @@ class PlatformAsset {
|
|||||||
this.createdAt,
|
this.createdAt,
|
||||||
this.updatedAt,
|
this.updatedAt,
|
||||||
required this.durationInSeconds,
|
required this.durationInSeconds,
|
||||||
required this.albumIds,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
String id;
|
String id;
|
||||||
@ -53,8 +51,6 @@ class PlatformAsset {
|
|||||||
|
|
||||||
int durationInSeconds;
|
int durationInSeconds;
|
||||||
|
|
||||||
List<String> albumIds;
|
|
||||||
|
|
||||||
List<Object?> _toList() {
|
List<Object?> _toList() {
|
||||||
return <Object?>[
|
return <Object?>[
|
||||||
id,
|
id,
|
||||||
@ -63,13 +59,11 @@ class PlatformAsset {
|
|||||||
createdAt,
|
createdAt,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
durationInSeconds,
|
durationInSeconds,
|
||||||
albumIds,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
Object encode() {
|
Object encode() {
|
||||||
return _toList();
|
return _toList(); }
|
||||||
}
|
|
||||||
|
|
||||||
static PlatformAsset decode(Object result) {
|
static PlatformAsset decode(Object result) {
|
||||||
result as List<Object?>;
|
result as List<Object?>;
|
||||||
@ -80,7 +74,6 @@ class PlatformAsset {
|
|||||||
createdAt: result[3] as int?,
|
createdAt: result[3] as int?,
|
||||||
updatedAt: result[4] as int?,
|
updatedAt: result[4] as int?,
|
||||||
durationInSeconds: result[5]! as int,
|
durationInSeconds: result[5]! as int,
|
||||||
albumIds: (result[6] as List<Object?>?)!.cast<String>(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,14 +91,16 @@ class PlatformAsset {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||||
int get hashCode => Object.hashAll(_toList());
|
int get hashCode => Object.hashAll(_toList())
|
||||||
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SyncDelta {
|
class SyncDelta {
|
||||||
SyncDelta({
|
SyncDelta({
|
||||||
this.hasChanges = false,
|
required this.hasChanges,
|
||||||
this.updates = const [],
|
required this.updates,
|
||||||
this.deletes = const [],
|
required this.deletes,
|
||||||
|
required this.albumAssets,
|
||||||
});
|
});
|
||||||
|
|
||||||
bool hasChanges;
|
bool hasChanges;
|
||||||
@ -114,17 +109,19 @@ class SyncDelta {
|
|||||||
|
|
||||||
List<String> deletes;
|
List<String> deletes;
|
||||||
|
|
||||||
|
Map<String, List<String>> albumAssets;
|
||||||
|
|
||||||
List<Object?> _toList() {
|
List<Object?> _toList() {
|
||||||
return <Object?>[
|
return <Object?>[
|
||||||
hasChanges,
|
hasChanges,
|
||||||
updates,
|
updates,
|
||||||
deletes,
|
deletes,
|
||||||
|
albumAssets,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
Object encode() {
|
Object encode() {
|
||||||
return _toList();
|
return _toList(); }
|
||||||
}
|
|
||||||
|
|
||||||
static SyncDelta decode(Object result) {
|
static SyncDelta decode(Object result) {
|
||||||
result as List<Object?>;
|
result as List<Object?>;
|
||||||
@ -132,6 +129,7 @@ class SyncDelta {
|
|||||||
hasChanges: result[0]! as bool,
|
hasChanges: result[0]! as bool,
|
||||||
updates: (result[1] as List<Object?>?)!.cast<PlatformAsset>(),
|
updates: (result[1] as List<Object?>?)!.cast<PlatformAsset>(),
|
||||||
deletes: (result[2] as List<Object?>?)!.cast<String>(),
|
deletes: (result[2] as List<Object?>?)!.cast<String>(),
|
||||||
|
albumAssets: (result[3] as Map<Object?, Object?>?)!.cast<String, List<String>>(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,9 +147,11 @@ class SyncDelta {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||||
int get hashCode => Object.hashAll(_toList());
|
int get hashCode => Object.hashAll(_toList())
|
||||||
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class _PigeonCodec extends StandardMessageCodec {
|
class _PigeonCodec extends StandardMessageCodec {
|
||||||
const _PigeonCodec();
|
const _PigeonCodec();
|
||||||
@override
|
@override
|
||||||
@ -159,10 +159,10 @@ class _PigeonCodec extends StandardMessageCodec {
|
|||||||
if (value is int) {
|
if (value is int) {
|
||||||
buffer.putUint8(4);
|
buffer.putUint8(4);
|
||||||
buffer.putInt64(value);
|
buffer.putInt64(value);
|
||||||
} else if (value is PlatformAsset) {
|
} else if (value is PlatformAsset) {
|
||||||
buffer.putUint8(129);
|
buffer.putUint8(129);
|
||||||
writeValue(buffer, value.encode());
|
writeValue(buffer, value.encode());
|
||||||
} else if (value is SyncDelta) {
|
} else if (value is SyncDelta) {
|
||||||
buffer.putUint8(130);
|
buffer.putUint8(130);
|
||||||
writeValue(buffer, value.encode());
|
writeValue(buffer, value.encode());
|
||||||
} else {
|
} else {
|
||||||
@ -173,9 +173,9 @@ class _PigeonCodec extends StandardMessageCodec {
|
|||||||
@override
|
@override
|
||||||
Object? readValueOfType(int type, ReadBuffer buffer) {
|
Object? readValueOfType(int type, ReadBuffer buffer) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 129:
|
case 129:
|
||||||
return PlatformAsset.decode(readValue(buffer)!);
|
return PlatformAsset.decode(readValue(buffer)!);
|
||||||
case 130:
|
case 130:
|
||||||
return SyncDelta.decode(readValue(buffer)!);
|
return SyncDelta.decode(readValue(buffer)!);
|
||||||
default:
|
default:
|
||||||
return super.readValueOfType(type, buffer);
|
return super.readValueOfType(type, buffer);
|
||||||
@ -183,15 +183,13 @@ class _PigeonCodec extends StandardMessageCodec {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ImHostService {
|
class ImHostApi {
|
||||||
/// Constructor for [ImHostService]. The [binaryMessenger] named argument is
|
/// Constructor for [ImHostApi]. The [binaryMessenger] named argument is
|
||||||
/// available for dependency injection. If it is left null, the default
|
/// available for dependency injection. If it is left null, the default
|
||||||
/// BinaryMessenger will be used which routes to the host platform.
|
/// BinaryMessenger will be used which routes to the host platform.
|
||||||
ImHostService(
|
ImHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
||||||
{BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
|
||||||
: pigeonVar_binaryMessenger = binaryMessenger,
|
: pigeonVar_binaryMessenger = binaryMessenger,
|
||||||
pigeonVar_messageChannelSuffix =
|
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||||
messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
|
||||||
final BinaryMessenger? pigeonVar_binaryMessenger;
|
final BinaryMessenger? pigeonVar_binaryMessenger;
|
||||||
|
|
||||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||||
@ -199,10 +197,8 @@ class ImHostService {
|
|||||||
final String pigeonVar_messageChannelSuffix;
|
final String pigeonVar_messageChannelSuffix;
|
||||||
|
|
||||||
Future<bool> shouldFullSync() async {
|
Future<bool> shouldFullSync() async {
|
||||||
final String pigeonVar_channelName =
|
final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.ImHostApi.shouldFullSync$pigeonVar_messageChannelSuffix';
|
||||||
'dev.flutter.pigeon.immich_mobile.ImHostService.shouldFullSync$pigeonVar_messageChannelSuffix';
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
final BasicMessageChannel<Object?> pigeonVar_channel =
|
|
||||||
BasicMessageChannel<Object?>(
|
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
binaryMessenger: pigeonVar_binaryMessenger,
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
@ -229,10 +225,8 @@ class ImHostService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<SyncDelta> getMediaChanges() async {
|
Future<SyncDelta> getMediaChanges() async {
|
||||||
final String pigeonVar_channelName =
|
final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.ImHostApi.getMediaChanges$pigeonVar_messageChannelSuffix';
|
||||||
'dev.flutter.pigeon.immich_mobile.ImHostService.getMediaChanges$pigeonVar_messageChannelSuffix';
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
final BasicMessageChannel<Object?> pigeonVar_channel =
|
|
||||||
BasicMessageChannel<Object?>(
|
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
binaryMessenger: pigeonVar_binaryMessenger,
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
@ -259,10 +253,8 @@ class ImHostService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> checkpointSync() async {
|
Future<void> checkpointSync() async {
|
||||||
final String pigeonVar_channelName =
|
final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.ImHostApi.checkpointSync$pigeonVar_messageChannelSuffix';
|
||||||
'dev.flutter.pigeon.immich_mobile.ImHostService.checkpointSync$pigeonVar_messageChannelSuffix';
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
final BasicMessageChannel<Object?> pigeonVar_channel =
|
|
||||||
BasicMessageChannel<Object?>(
|
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
binaryMessenger: pigeonVar_binaryMessenger,
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
@ -284,10 +276,8 @@ class ImHostService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> clearSyncCheckpoint() async {
|
Future<void> clearSyncCheckpoint() async {
|
||||||
final String pigeonVar_channelName =
|
final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.ImHostApi.clearSyncCheckpoint$pigeonVar_messageChannelSuffix';
|
||||||
'dev.flutter.pigeon.immich_mobile.ImHostService.clearSyncCheckpoint$pigeonVar_messageChannelSuffix';
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
final BasicMessageChannel<Object?> pigeonVar_channel =
|
|
||||||
BasicMessageChannel<Object?>(
|
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
binaryMessenger: pigeonVar_binaryMessenger,
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
@ -309,16 +299,13 @@ class ImHostService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<List<String>> getAssetIdsForAlbum(String albumId) async {
|
Future<List<String>> getAssetIdsForAlbum(String albumId) async {
|
||||||
final String pigeonVar_channelName =
|
final String pigeonVar_channelName = 'dev.flutter.pigeon.immich_mobile.ImHostApi.getAssetIdsForAlbum$pigeonVar_messageChannelSuffix';
|
||||||
'dev.flutter.pigeon.immich_mobile.ImHostService.getAssetIdsForAlbum$pigeonVar_messageChannelSuffix';
|
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
final BasicMessageChannel<Object?> pigeonVar_channel =
|
|
||||||
BasicMessageChannel<Object?>(
|
|
||||||
pigeonVar_channelName,
|
pigeonVar_channelName,
|
||||||
pigeonChannelCodec,
|
pigeonChannelCodec,
|
||||||
binaryMessenger: pigeonVar_binaryMessenger,
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
);
|
);
|
||||||
final Future<Object?> pigeonVar_sendFuture =
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[albumId]);
|
||||||
pigeonVar_channel.send(<Object?>[albumId]);
|
|
||||||
final List<Object?>? pigeonVar_replyList =
|
final List<Object?>? pigeonVar_replyList =
|
||||||
await pigeonVar_sendFuture as List<Object?>?;
|
await pigeonVar_sendFuture as List<Object?>?;
|
||||||
if (pigeonVar_replyList == null) {
|
if (pigeonVar_replyList == null) {
|
||||||
|
@ -30,7 +30,7 @@ final _features = [
|
|||||||
_Feature(
|
_Feature(
|
||||||
name: 'Clear Delta Checkpoint',
|
name: 'Clear Delta Checkpoint',
|
||||||
icon: Icons.delete_rounded,
|
icon: Icons.delete_rounded,
|
||||||
onTap: (_, ref) => ref.read(hostServiceProvider).clearSyncCheckpoint(),
|
onTap: (_, ref) => ref.read(hostApiProvider).clearSyncCheckpoint(),
|
||||||
),
|
),
|
||||||
_Feature(
|
_Feature(
|
||||||
name: 'Clear Local Data',
|
name: 'Clear Local Data',
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/platform/messages.g.dart';
|
import 'package:immich_mobile/platform/messages.g.dart';
|
||||||
|
|
||||||
final hostServiceProvider = Provider<ImHostService>((_) => ImHostService());
|
final hostApiProvider = Provider<ImHostApi>((_) => ImHostApi());
|
||||||
|
@ -13,7 +13,7 @@ final deviceSyncServiceProvider = Provider(
|
|||||||
(ref) => DeviceSyncService(
|
(ref) => DeviceSyncService(
|
||||||
albumMediaRepository: ref.watch(albumMediaRepositoryProvider),
|
albumMediaRepository: ref.watch(albumMediaRepositoryProvider),
|
||||||
localAlbumRepository: ref.watch(localAlbumRepository),
|
localAlbumRepository: ref.watch(localAlbumRepository),
|
||||||
hostService: ref.watch(hostServiceProvider),
|
hostApi: ref.watch(hostApiProvider),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -11,6 +11,6 @@ class MockUserService extends Mock implements UserService {}
|
|||||||
|
|
||||||
class MockBackgroundSyncManager extends Mock implements BackgroundSyncManager {}
|
class MockBackgroundSyncManager extends Mock implements BackgroundSyncManager {}
|
||||||
|
|
||||||
class MockHostService extends Mock implements ImHostService {}
|
class MockHostApi extends Mock implements ImHostApi {}
|
||||||
|
|
||||||
class MockPlatform extends Mock implements Platform {}
|
class MockPlatform extends Mock implements Platform {}
|
||||||
|
@ -15,8 +15,8 @@ import '../service.mock.dart';
|
|||||||
void main() {
|
void main() {
|
||||||
late IAlbumMediaRepository mockAlbumMediaRepo;
|
late IAlbumMediaRepository mockAlbumMediaRepo;
|
||||||
late ILocalAlbumRepository mockLocalAlbumRepo;
|
late ILocalAlbumRepository mockLocalAlbumRepo;
|
||||||
late ImHostService mockHostService;
|
late ImHostApi mockHostApi;
|
||||||
late MockPlatform mockPlatformInstance;
|
late MockPlatform mockPlatform;
|
||||||
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,20 +24,20 @@ void main() {
|
|||||||
setUp(() {
|
setUp(() {
|
||||||
mockAlbumMediaRepo = MockAlbumMediaRepository();
|
mockAlbumMediaRepo = MockAlbumMediaRepository();
|
||||||
mockLocalAlbumRepo = MockLocalAlbumRepository();
|
mockLocalAlbumRepo = MockLocalAlbumRepository();
|
||||||
mockHostService = MockHostService();
|
mockHostApi = MockHostApi();
|
||||||
mockPlatformInstance = MockPlatform();
|
mockPlatform = MockPlatform();
|
||||||
|
|
||||||
sut = DeviceSyncService(
|
sut = DeviceSyncService(
|
||||||
albumMediaRepository: mockAlbumMediaRepo,
|
albumMediaRepository: mockAlbumMediaRepo,
|
||||||
localAlbumRepository: mockLocalAlbumRepo,
|
localAlbumRepository: mockLocalAlbumRepo,
|
||||||
hostService: mockHostService,
|
hostApi: mockHostApi,
|
||||||
platform: mockPlatformInstance,
|
platform: mockPlatform,
|
||||||
);
|
);
|
||||||
|
|
||||||
registerFallbackValue(LocalAlbumStub.album1);
|
registerFallbackValue(LocalAlbumStub.album1);
|
||||||
registerFallbackValue(LocalAssetStub.image1);
|
registerFallbackValue(LocalAssetStub.image1);
|
||||||
registerFallbackValue(
|
registerFallbackValue(
|
||||||
SyncDelta(hasChanges: true, updates: [], deletes: []),
|
SyncDelta(hasChanges: true, updates: [], deletes: [], albumAssets: {}),
|
||||||
);
|
);
|
||||||
|
|
||||||
when(() => mockAlbumMediaRepo.getAll()).thenAnswer((_) async => []);
|
when(() => mockAlbumMediaRepo.getAll()).thenAnswer((_) async => []);
|
||||||
@ -82,13 +82,18 @@ void main() {
|
|||||||
|
|
||||||
when(() => mockHostService.shouldFullSync()).thenAnswer((_) async => true);
|
when(() => mockHostService.shouldFullSync()).thenAnswer((_) async => true);
|
||||||
when(() => mockHostService.getMediaChanges()).thenAnswer(
|
when(() => mockHostService.getMediaChanges()).thenAnswer(
|
||||||
(_) async => SyncDelta(hasChanges: false, updates: [], deletes: []),
|
(_) async => SyncDelta(
|
||||||
|
hasChanges: false,
|
||||||
|
updates: [],
|
||||||
|
deletes: [],
|
||||||
|
albumAssets: {},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
when(() => mockHostService.getAssetIdsForAlbum(any()))
|
when(() => mockHostService.getAssetIdsForAlbum(any()))
|
||||||
.thenAnswer((_) async => []);
|
.thenAnswer((_) async => []);
|
||||||
when(() => mockHostService.checkpointSync()).thenAnswer((_) async => {});
|
when(() => mockHostService.checkpointSync()).thenAnswer((_) async => {});
|
||||||
|
|
||||||
when(() => mockPlatformInstance.isAndroid).thenReturn(false);
|
when(() => mockPlatform.isAndroid).thenReturn(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
group('sync', () {
|
group('sync', () {
|
||||||
@ -119,7 +124,12 @@ void main() {
|
|||||||
when(() => mockHostService.shouldFullSync())
|
when(() => mockHostService.shouldFullSync())
|
||||||
.thenAnswer((_) async => false);
|
.thenAnswer((_) async => false);
|
||||||
when(() => mockHostService.getMediaChanges()).thenAnswer(
|
when(() => mockHostService.getMediaChanges()).thenAnswer(
|
||||||
(_) async => SyncDelta(hasChanges: false, updates: [], deletes: []),
|
(_) async => SyncDelta(
|
||||||
|
hasChanges: false,
|
||||||
|
updates: [],
|
||||||
|
deletes: [],
|
||||||
|
albumAssets: {},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await sut.sync();
|
await sut.sync();
|
||||||
@ -140,6 +150,9 @@ void main() {
|
|||||||
hasChanges: true,
|
hasChanges: true,
|
||||||
updates: [PlatformAssetStub.image1],
|
updates: [PlatformAssetStub.image1],
|
||||||
deletes: ["deleted"],
|
deletes: ["deleted"],
|
||||||
|
albumAssets: {
|
||||||
|
"albumId": ["asset1", "asset2"],
|
||||||
|
},
|
||||||
);
|
);
|
||||||
final deviceAlbums = [LocalAlbumStub.album1];
|
final deviceAlbums = [LocalAlbumStub.album1];
|
||||||
|
|
||||||
@ -149,7 +162,7 @@ void main() {
|
|||||||
.thenAnswer((_) async => delta);
|
.thenAnswer((_) async => delta);
|
||||||
when(() => mockAlbumMediaRepo.getAll())
|
when(() => mockAlbumMediaRepo.getAll())
|
||||||
.thenAnswer((_) async => deviceAlbums);
|
.thenAnswer((_) async => deviceAlbums);
|
||||||
when(() => mockPlatformInstance.isAndroid).thenReturn(false);
|
when(() => mockPlatform.isAndroid).thenReturn(false);
|
||||||
|
|
||||||
await sut.sync();
|
await sut.sync();
|
||||||
|
|
||||||
@ -172,6 +185,9 @@ void main() {
|
|||||||
hasChanges: true,
|
hasChanges: true,
|
||||||
updates: [PlatformAssetStub.image1],
|
updates: [PlatformAssetStub.image1],
|
||||||
deletes: ["deleted"],
|
deletes: ["deleted"],
|
||||||
|
albumAssets: {
|
||||||
|
"dbAlbumId": ["asset1", "asset2"],
|
||||||
|
},
|
||||||
);
|
);
|
||||||
final deviceAlbums = [LocalAlbumStub.album1];
|
final deviceAlbums = [LocalAlbumStub.album1];
|
||||||
final dbAlbums = [LocalAlbumStub.album2.copyWith(id: "dbAlbumId")];
|
final dbAlbums = [LocalAlbumStub.album2.copyWith(id: "dbAlbumId")];
|
||||||
@ -185,7 +201,7 @@ void main() {
|
|||||||
.thenAnswer((_) async => deviceAlbums);
|
.thenAnswer((_) async => deviceAlbums);
|
||||||
when(() => mockLocalAlbumRepo.getAll())
|
when(() => mockLocalAlbumRepo.getAll())
|
||||||
.thenAnswer((_) async => dbAlbums);
|
.thenAnswer((_) async => dbAlbums);
|
||||||
when(() => mockPlatformInstance.isAndroid).thenReturn(true);
|
when(() => mockPlatform.isAndroid).thenReturn(true);
|
||||||
when(() => mockHostService.getAssetIdsForAlbum(dbAlbums.first.id))
|
when(() => mockHostService.getAssetIdsForAlbum(dbAlbums.first.id))
|
||||||
.thenAnswer((_) async => assetIdsForDbAlbum);
|
.thenAnswer((_) async => assetIdsForDbAlbum);
|
||||||
|
|
||||||
@ -197,7 +213,7 @@ void main() {
|
|||||||
() => mockAlbumMediaRepo.getAll(),
|
() => mockAlbumMediaRepo.getAll(),
|
||||||
() => mockLocalAlbumRepo.updateAll(deviceAlbums),
|
() => mockLocalAlbumRepo.updateAll(deviceAlbums),
|
||||||
() => mockLocalAlbumRepo.processDelta(delta),
|
() => mockLocalAlbumRepo.processDelta(delta),
|
||||||
() => mockPlatformInstance.isAndroid,
|
() => mockPlatform.isAndroid,
|
||||||
() => mockLocalAlbumRepo.getAll(),
|
() => mockLocalAlbumRepo.getAll(),
|
||||||
() => mockHostService.getAssetIdsForAlbum(dbAlbums.first.id),
|
() => mockHostService.getAssetIdsForAlbum(dbAlbums.first.id),
|
||||||
() => mockLocalAlbumRepo.syncAlbumDeletes(
|
() => mockLocalAlbumRepo.syncAlbumDeletes(
|
||||||
|
@ -8,7 +8,6 @@ abstract final class PlatformAssetStub {
|
|||||||
createdAt: DateTime(2024, 1, 1).millisecondsSinceEpoch,
|
createdAt: DateTime(2024, 1, 1).millisecondsSinceEpoch,
|
||||||
updatedAt: DateTime(2024, 1, 1).millisecondsSinceEpoch,
|
updatedAt: DateTime(2024, 1, 1).millisecondsSinceEpoch,
|
||||||
durationInSeconds: 0,
|
durationInSeconds: 0,
|
||||||
albumIds: ["album1"],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
static PlatformAsset get video1 => PlatformAsset(
|
static PlatformAsset get video1 => PlatformAsset(
|
||||||
@ -18,6 +17,5 @@ abstract final class PlatformAssetStub {
|
|||||||
createdAt: DateTime(2024, 1, 2).millisecondsSinceEpoch,
|
createdAt: DateTime(2024, 1, 2).millisecondsSinceEpoch,
|
||||||
updatedAt: DateTime(2024, 1, 2).millisecondsSinceEpoch,
|
updatedAt: DateTime(2024, 1, 2).millisecondsSinceEpoch,
|
||||||
durationInSeconds: 120,
|
durationInSeconds: 120,
|
||||||
albumIds: ["album1"],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user