mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
optimize, refactor code
remove redundant code and checking getTrashedAssetsForAlbum for iOS tests for hash trashed assets
This commit is contained in:
parent
3839e72028
commit
3eb2bf0342
24 changed files with 393 additions and 793 deletions
|
|
@ -89,9 +89,7 @@ data class PlatformAsset (
|
|||
val height: Long? = null,
|
||||
val durationInSeconds: Long,
|
||||
val orientation: Long,
|
||||
val isFavorite: Boolean,
|
||||
val isTrashed: Boolean? = null,
|
||||
val volume: String? = null
|
||||
val isFavorite: Boolean
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
|
|
@ -106,9 +104,7 @@ data class PlatformAsset (
|
|||
val durationInSeconds = pigeonVar_list[7] as Long
|
||||
val orientation = pigeonVar_list[8] as Long
|
||||
val isFavorite = pigeonVar_list[9] as Boolean
|
||||
val isTrashed = pigeonVar_list[10] as Boolean?
|
||||
val volume = pigeonVar_list[11] as String?
|
||||
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite, isTrashed, volume)
|
||||
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
|
|
@ -123,8 +119,6 @@ data class PlatformAsset (
|
|||
durationInSeconds,
|
||||
orientation,
|
||||
isFavorite,
|
||||
isTrashed,
|
||||
volume,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
|
|
@ -249,40 +243,6 @@ data class HashResult (
|
|||
|
||||
override fun hashCode(): Int = toList().hashCode()
|
||||
}
|
||||
|
||||
/** Generated class from Pigeon that represents data sent in messages. */
|
||||
data class TrashedAssetParams (
|
||||
val id: String,
|
||||
val type: Long,
|
||||
val albumId: String? = null
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
fun fromList(pigeonVar_list: List<Any?>): TrashedAssetParams {
|
||||
val id = pigeonVar_list[0] as String
|
||||
val type = pigeonVar_list[1] as Long
|
||||
val albumId = pigeonVar_list[2] as String?
|
||||
return TrashedAssetParams(id, type, albumId)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
return listOf(
|
||||
id,
|
||||
type,
|
||||
albumId,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is TrashedAssetParams) {
|
||||
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) {
|
||||
|
|
@ -306,11 +266,6 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
|
|||
HashResult.fromList(it)
|
||||
}
|
||||
}
|
||||
133.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
TrashedAssetParams.fromList(it)
|
||||
}
|
||||
}
|
||||
else -> super.readValueOfType(type, buffer)
|
||||
}
|
||||
}
|
||||
|
|
@ -332,10 +287,6 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
|
|||
stream.write(132)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is TrashedAssetParams -> {
|
||||
stream.write(133)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
else -> super.writeValue(stream, value)
|
||||
}
|
||||
}
|
||||
|
|
@ -355,7 +306,6 @@ interface NativeSyncApi {
|
|||
fun hashAssets(assetIds: List<String>, allowNetworkAccess: Boolean, callback: (Result<List<HashResult>>) -> Unit)
|
||||
fun cancelHashing()
|
||||
fun getTrashedAssetsForAlbum(albumId: String): List<PlatformAsset>
|
||||
fun hashTrashedAssets(trashedAssets: List<TrashedAssetParams>, callback: (Result<List<HashResult>>) -> Unit)
|
||||
|
||||
companion object {
|
||||
/** The codec used by NativeSyncApi. */
|
||||
|
|
@ -553,26 +503,6 @@ interface NativeSyncApi {
|
|||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashTrashedAssets$separatedMessageChannelSuffix", codec, taskQueue)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val trashedAssetsArg = args[0] as List<TrashedAssetParams>
|
||||
api.hashTrashedAssets(trashedAssetsArg) { result: Result<List<HashResult>> ->
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,11 +27,4 @@ class NativeSyncApiImpl26(context: Context) : NativeSyncApiImplBase(context), Na
|
|||
): List<PlatformAsset> {
|
||||
throw IllegalStateException("Method not supported on this Android version.")
|
||||
}
|
||||
|
||||
override fun hashTrashedAssets(
|
||||
trashedAssets: List<TrashedAssetParams>,
|
||||
callback: (Result<List<HashResult>>) -> Unit
|
||||
) {
|
||||
throw IllegalStateException("Method not supported on this Android version.")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,59 +130,4 @@ class NativeSyncApiImpl30(context: Context) : NativeSyncApiImplBase(context), Na
|
|||
|
||||
return trashed
|
||||
}
|
||||
|
||||
override fun hashTrashedAssets(
|
||||
trashedAssets: List<TrashedAssetParams>,
|
||||
callback: (Result<List<HashResult>>) -> Unit
|
||||
) {
|
||||
if (trashedAssets.isEmpty()) {
|
||||
callback(Result.success(emptyList()))
|
||||
return
|
||||
}
|
||||
hashTask?.cancel()
|
||||
hashTask = CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val results = trashedAssets.map { assetParams ->
|
||||
async {
|
||||
hashSemaphore.withPermit {
|
||||
ensureActive()
|
||||
hashTrashedAsset(assetParams)
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
|
||||
callback(Result.success(results))
|
||||
} catch (e: CancellationException) {
|
||||
callback(
|
||||
Result.failure(
|
||||
FlutterError(
|
||||
HASHING_CANCELLED_CODE,
|
||||
"Hashing operation was cancelled",
|
||||
null
|
||||
)
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
callback(Result.failure(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun hashTrashedAsset(assetParams: TrashedAssetParams): HashResult {
|
||||
val id = assetParams.id.toLong()
|
||||
val mediaType = assetParams.type.toInt()
|
||||
val assetUri = contentUriForType(id, mediaType)
|
||||
return hashAssetFromUri(assetParams.id, assetUri)
|
||||
}
|
||||
|
||||
private fun contentUriForType(id: Long, mediaType: Int): Uri {
|
||||
val vol = MediaStore.VOLUME_EXTERNAL
|
||||
val base = when (mediaType) {
|
||||
MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE -> MediaStore.Images.Media.getContentUri(vol)
|
||||
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO -> MediaStore.Video.Media.getContentUri(vol)
|
||||
MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO -> MediaStore.Audio.Media.getContentUri(vol)
|
||||
else -> MediaStore.Files.getContentUri(vol)
|
||||
}
|
||||
return ContentUris.withAppendedId(base, id)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,8 +62,6 @@ open class NativeSyncApiImplBase(context: Context) {
|
|||
// only available on Android 11 and above
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||
add(MediaStore.MediaColumns.IS_FAVORITE)
|
||||
add(MediaStore.MediaColumns.IS_TRASHED)
|
||||
add(MediaStore.MediaColumns.VOLUME_NAME)
|
||||
}
|
||||
}.toTypedArray()
|
||||
|
||||
|
|
@ -111,8 +109,6 @@ open class NativeSyncApiImplBase(context: Context) {
|
|||
val orientationColumn =
|
||||
c.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION)
|
||||
val favoriteColumn = c.getColumnIndex(MediaStore.MediaColumns.IS_FAVORITE)
|
||||
val trashedColumn = c.getColumnIndex(MediaStore.MediaColumns.IS_TRASHED)
|
||||
val volumeColumn = c.getColumnIndex(MediaStore.MediaColumns.VOLUME_NAME)
|
||||
|
||||
while (c.moveToNext()) {
|
||||
val id = c.getLong(idColumn).toString()
|
||||
|
|
@ -142,8 +138,6 @@ open class NativeSyncApiImplBase(context: Context) {
|
|||
val bucketId = c.getString(bucketIdColumn)
|
||||
val orientation = c.getInt(orientationColumn)
|
||||
val isFavorite = if (favoriteColumn == -1) false else c.getInt(favoriteColumn) != 0
|
||||
val isTrashed = if (trashedColumn == -1) null else c.getInt(trashedColumn) != 0
|
||||
val volume = if (volumeColumn == -1) null else c.getString(volumeColumn)
|
||||
val asset = PlatformAsset(
|
||||
id,
|
||||
name,
|
||||
|
|
@ -155,8 +149,6 @@ open class NativeSyncApiImplBase(context: Context) {
|
|||
duration,
|
||||
orientation.toLong(),
|
||||
isFavorite,
|
||||
isTrashed,
|
||||
volume,
|
||||
)
|
||||
yield(AssetResult.ValidAsset(asset, bucketId))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -246,46 +246,46 @@ EXTERNAL SOURCES:
|
|||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad
|
||||
bonsoir_darwin: 29c7ccf356646118844721f36e1de4b61f6cbd0e
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||
background_downloader: a05c77d32a0d70615b9c04577aa203535fc924ff
|
||||
bonsoir_darwin: e3b8526c42ca46a885142df84229131dfabea842
|
||||
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
|
||||
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
||||
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
|
||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
||||
flutter_web_auth_2: 5c8d9dcd7848b5a9efb086d24e7a9adcae979c80
|
||||
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
|
||||
geolocator_apple: 1560c3c875af2a412242c7a923e15d0d401966ff
|
||||
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||
isar_flutter_libs: bc909e72c3d756c2759f14c8776c13b5b0556e26
|
||||
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
|
||||
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
||||
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
|
||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
|
||||
flutter_web_auth_2: 06d500582775790a0d4c323222fcb6d7990f9603
|
||||
fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
|
||||
geolocator_apple: 9bcea1918ff7f0062d98345d238ae12718acfbc1
|
||||
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
|
||||
isar_flutter_libs: fdf730ca925d05687f36d7f1d355e482529ed097
|
||||
local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
|
||||
MapLibre: 69e572367f4ef6287e18246cfafc39c80cdcabcd
|
||||
maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f
|
||||
native_video_player: b65c58951ede2f93d103a25366bdebca95081265
|
||||
network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413
|
||||
maplibre_gl: 753f55d763a81cbdba087d02af02d12206e6f94e
|
||||
native_video_player: d12af78a1a4a8cf09775a5177d5b392def6fd23c
|
||||
network_info_plus: 6613d9d7cdeb0e6f366ed4dbe4b3c51c52d567a9
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
|
||||
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
|
||||
share_handler_ios: 6dd3a4ac5ca0d955274aec712ba0ecdcaf583e7c
|
||||
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
||||
sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241
|
||||
sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56
|
||||
|
||||
PODFILE CHECKSUM: 7ce312f2beab01395db96f6969d90a447279cf45
|
||||
|
||||
|
|
|
|||
|
|
@ -140,8 +140,6 @@ struct PlatformAsset: Hashable {
|
|||
var durationInSeconds: Int64
|
||||
var orientation: Int64
|
||||
var isFavorite: Bool
|
||||
var isTrashed: Bool? = nil
|
||||
var volume: String? = nil
|
||||
|
||||
|
||||
// swift-format-ignore: AlwaysUseLowerCamelCase
|
||||
|
|
@ -156,8 +154,6 @@ struct PlatformAsset: Hashable {
|
|||
let durationInSeconds = pigeonVar_list[7] as! Int64
|
||||
let orientation = pigeonVar_list[8] as! Int64
|
||||
let isFavorite = pigeonVar_list[9] as! Bool
|
||||
let isTrashed: Bool? = nilOrValue(pigeonVar_list[10])
|
||||
let volume: String? = nilOrValue(pigeonVar_list[11])
|
||||
|
||||
return PlatformAsset(
|
||||
id: id,
|
||||
|
|
@ -169,9 +165,7 @@ struct PlatformAsset: Hashable {
|
|||
height: height,
|
||||
durationInSeconds: durationInSeconds,
|
||||
orientation: orientation,
|
||||
isFavorite: isFavorite,
|
||||
isTrashed: isTrashed,
|
||||
volume: volume
|
||||
isFavorite: isFavorite
|
||||
)
|
||||
}
|
||||
func toList() -> [Any?] {
|
||||
|
|
@ -186,8 +180,6 @@ struct PlatformAsset: Hashable {
|
|||
durationInSeconds,
|
||||
orientation,
|
||||
isFavorite,
|
||||
isTrashed,
|
||||
volume,
|
||||
]
|
||||
}
|
||||
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
|
||||
|
|
@ -308,39 +300,6 @@ struct HashResult: Hashable {
|
|||
}
|
||||
}
|
||||
|
||||
/// Generated class from Pigeon that represents data sent in messages.
|
||||
struct TrashedAssetParams: Hashable {
|
||||
var id: String
|
||||
var type: Int64
|
||||
var albumId: String? = nil
|
||||
|
||||
|
||||
// swift-format-ignore: AlwaysUseLowerCamelCase
|
||||
static func fromList(_ pigeonVar_list: [Any?]) -> TrashedAssetParams? {
|
||||
let id = pigeonVar_list[0] as! String
|
||||
let type = pigeonVar_list[1] as! Int64
|
||||
let albumId: String? = nilOrValue(pigeonVar_list[2])
|
||||
|
||||
return TrashedAssetParams(
|
||||
id: id,
|
||||
type: type,
|
||||
albumId: albumId
|
||||
)
|
||||
}
|
||||
func toList() -> [Any?] {
|
||||
return [
|
||||
id,
|
||||
type,
|
||||
albumId,
|
||||
]
|
||||
}
|
||||
static func == (lhs: TrashedAssetParams, rhs: TrashedAssetParams) -> 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 {
|
||||
|
|
@ -352,8 +311,6 @@ private class MessagesPigeonCodecReader: FlutterStandardReader {
|
|||
return SyncDelta.fromList(self.readValue() as! [Any?])
|
||||
case 132:
|
||||
return HashResult.fromList(self.readValue() as! [Any?])
|
||||
case 133:
|
||||
return TrashedAssetParams.fromList(self.readValue() as! [Any?])
|
||||
default:
|
||||
return super.readValue(ofType: type)
|
||||
}
|
||||
|
|
@ -374,9 +331,6 @@ private class MessagesPigeonCodecWriter: FlutterStandardWriter {
|
|||
} else if let value = value as? HashResult {
|
||||
super.writeByte(132)
|
||||
super.writeValue(value.toList())
|
||||
} else if let value = value as? TrashedAssetParams {
|
||||
super.writeByte(133)
|
||||
super.writeValue(value.toList())
|
||||
} else {
|
||||
super.writeValue(value)
|
||||
}
|
||||
|
|
@ -411,7 +365,6 @@ protocol NativeSyncApi {
|
|||
func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void)
|
||||
func cancelHashing() throws
|
||||
func getTrashedAssetsForAlbum(albumId: String) throws -> [PlatformAsset]
|
||||
func hashTrashedAssets(trashedAssets: [TrashedAssetParams], completion: @escaping (Result<[HashResult], Error>) -> Void)
|
||||
}
|
||||
|
||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||
|
|
@ -599,24 +552,5 @@ class NativeSyncApiSetup {
|
|||
} else {
|
||||
getTrashedAssetsForAlbumChannel.setMessageHandler(nil)
|
||||
}
|
||||
let hashTrashedAssetsChannel = taskQueue == nil
|
||||
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashTrashedAssets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashTrashedAssets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
||||
if let api = api {
|
||||
hashTrashedAssetsChannel.setMessageHandler { message, reply in
|
||||
let args = message as! [Any?]
|
||||
let trashedAssetsArg = args[0] as! [TrashedAssetParams]
|
||||
api.hashTrashedAssets(trashedAssets: trashedAssetsArg) { result in
|
||||
switch result {
|
||||
case .success(let res):
|
||||
reply(wrapResult(res))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
hashTrashedAssetsChannel.setMessageHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ class NativeSyncApiImpl: NativeSyncApi {
|
|||
return albums.sorted { $0.id < $1.id }
|
||||
}
|
||||
|
||||
func getMediaChanges() throws -> SyncDelta {
|
||||
func getMediaChanges(isTrashed: Bool) throws -> SyncDelta {
|
||||
guard #available(iOS 16, *) else {
|
||||
throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature requires iOS 16 or later.", details: nil)
|
||||
}
|
||||
|
|
@ -363,4 +363,9 @@ class NativeSyncApiImpl: NativeSyncApi {
|
|||
PHAssetResourceManager.default().cancelDataRequest(requestId)
|
||||
})
|
||||
}
|
||||
|
||||
func getTrashedAssetsForAlbum(albumId: String) throws ->[PlatformAsset] {
|
||||
throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature not supported on iOS.", details: nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
|
||||
class TrashedAsset extends LocalAsset {
|
||||
final String? volume;
|
||||
final String albumId;
|
||||
|
||||
const TrashedAsset({
|
||||
required this.albumId,
|
||||
this.volume,
|
||||
required super.id,
|
||||
super.remoteId,
|
||||
required super.name,
|
||||
|
|
@ -38,7 +36,6 @@ class TrashedAsset extends LocalAsset {
|
|||
String? livePhotoVideoId,
|
||||
int? orientation,
|
||||
String? albumId,
|
||||
String? volume,
|
||||
}) {
|
||||
return TrashedAsset(
|
||||
id: id ?? this.id,
|
||||
|
|
@ -55,7 +52,6 @@ class TrashedAsset extends LocalAsset {
|
|||
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||
orientation: orientation ?? this.orientation,
|
||||
albumId: albumId ?? this.albumId,
|
||||
volume: volume ?? this.volume,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -79,7 +75,6 @@ class TrashedAsset extends LocalAsset {
|
|||
livePhotoVideoId == other.livePhotoVideoId &&
|
||||
orientation == other.orientation &&
|
||||
// TrashedAsset extras
|
||||
volume == other.volume &&
|
||||
albumId == other.albumId;
|
||||
|
||||
@override
|
||||
|
|
@ -97,7 +92,6 @@ class TrashedAsset extends LocalAsset {
|
|||
isFavorite,
|
||||
livePhotoVideoId,
|
||||
orientation,
|
||||
volume,
|
||||
albumId,
|
||||
);
|
||||
|
||||
|
|
@ -118,7 +112,6 @@ class TrashedAsset extends LocalAsset {
|
|||
'livePhotoVideoId: $livePhotoVideoId, '
|
||||
'orientation: $orientation, '
|
||||
'albumId: $albumId, '
|
||||
'volume: $volume'
|
||||
')';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import 'package:flutter/services.dart';
|
|||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/trashed_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/services/trash_sync.service.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
|
||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
|
|
@ -15,24 +15,24 @@ class HashService {
|
|||
final int _batchSize;
|
||||
final DriftLocalAlbumRepository _localAlbumRepository;
|
||||
final DriftLocalAssetRepository _localAssetRepository;
|
||||
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
||||
final NativeSyncApi _nativeSyncApi;
|
||||
final TrashSyncService _trashSyncService;
|
||||
final bool Function()? _cancelChecker;
|
||||
final _log = Logger('HashService');
|
||||
|
||||
HashService({
|
||||
required DriftLocalAlbumRepository localAlbumRepository,
|
||||
required DriftLocalAssetRepository localAssetRepository,
|
||||
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
|
||||
required NativeSyncApi nativeSyncApi,
|
||||
required TrashSyncService trashSyncService,
|
||||
bool Function()? cancelChecker,
|
||||
int? batchSize,
|
||||
}) : _localAlbumRepository = localAlbumRepository,
|
||||
_localAssetRepository = localAssetRepository,
|
||||
_trashedLocalAssetRepository = trashedLocalAssetRepository,
|
||||
_cancelChecker = cancelChecker,
|
||||
_nativeSyncApi = nativeSyncApi,
|
||||
_batchSize = batchSize ?? kBatchHashFileLimit,
|
||||
_trashSyncService = trashSyncService;
|
||||
_batchSize = batchSize ?? kBatchHashFileLimit;
|
||||
|
||||
bool get isCancelled => _cancelChecker?.call() ?? false;
|
||||
|
||||
|
|
@ -54,6 +54,14 @@ class HashService {
|
|||
await _hashAssets(album, assetsToHash);
|
||||
}
|
||||
}
|
||||
if (CurrentPlatform.isAndroid && localAlbums.isNotEmpty) {
|
||||
final backupAlbumIds = localAlbums.map((e) => e.id);
|
||||
final trashedToHash = await _trashedLocalAssetRepository.getAssetsToHash(backupAlbumIds);
|
||||
if (trashedToHash.isNotEmpty) {
|
||||
final pseudoAlbum = LocalAlbum(id: '-pseudoAlbum', name: 'Trash', updatedAt: DateTime.now());
|
||||
await _hashAssets(pseudoAlbum, trashedToHash.toList(), isTrashed: true);
|
||||
}
|
||||
}
|
||||
} on PlatformException catch (e) {
|
||||
if (e.code == _kHashCancelledCode) {
|
||||
_log.warning("Hashing cancelled by platform");
|
||||
|
|
@ -63,23 +71,6 @@ class HashService {
|
|||
_log.severe("Error during hashing", e, s);
|
||||
}
|
||||
|
||||
if (_trashSyncService.isAutoSyncMode) {
|
||||
final backupAlbums = await _localAlbumRepository.getBackupAlbums();
|
||||
if (backupAlbums.isNotEmpty) {
|
||||
final backupAlbumIds = backupAlbums.map((e) => e.id);
|
||||
final trashedToHash = await _trashSyncService.getAssetsToHash(backupAlbumIds);
|
||||
if (trashedToHash.isNotEmpty) {
|
||||
for (final album in backupAlbums) {
|
||||
if (isCancelled) {
|
||||
_log.warning("Hashing cancelled. Stopped processing albums.");
|
||||
break;
|
||||
}
|
||||
await _hashTrashedAssets(album, trashedToHash.where((e) => e.albumId == album.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stopwatch.stop();
|
||||
_log.info("Hashing took - ${stopwatch.elapsedMilliseconds}ms");
|
||||
}
|
||||
|
|
@ -87,7 +78,7 @@ class HashService {
|
|||
/// Processes a list of [LocalAsset]s, storing their hash and updating the assets in the DB
|
||||
/// with hash for those that were successfully hashed. Hashes are looked up in a table
|
||||
/// [LocalAssetHashEntity] by local id. Only missing entries are newly hashed and added to the DB.
|
||||
Future<void> _hashAssets(LocalAlbum album, List<LocalAsset> assetsToHash) async {
|
||||
Future<void> _hashAssets(LocalAlbum album, List<LocalAsset> assetsToHash, {bool isTrashed = false}) async {
|
||||
final toHash = <String, LocalAsset>{};
|
||||
|
||||
for (final asset in assetsToHash) {
|
||||
|
|
@ -98,16 +89,16 @@ class HashService {
|
|||
|
||||
toHash[asset.id] = asset;
|
||||
if (toHash.length == _batchSize) {
|
||||
await _processBatch(album, toHash);
|
||||
await _processBatch(album, toHash, isTrashed);
|
||||
toHash.clear();
|
||||
}
|
||||
}
|
||||
|
||||
await _processBatch(album, toHash);
|
||||
await _processBatch(album, toHash, isTrashed);
|
||||
}
|
||||
|
||||
/// Processes a batch of assets.
|
||||
Future<void> _processBatch(LocalAlbum album, Map<String, LocalAsset> toHash) async {
|
||||
Future<void> _processBatch(LocalAlbum album, Map<String, LocalAsset> toHash, bool isTrashed) async {
|
||||
if (toHash.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -142,64 +133,10 @@ class HashService {
|
|||
}
|
||||
|
||||
_log.fine("Hashed ${hashed.length}/${toHash.length} assets");
|
||||
|
||||
if (isTrashed) {
|
||||
await _trashedLocalAssetRepository.updateHashes(hashed);
|
||||
} else {
|
||||
await _localAssetRepository.updateHashes(hashed);
|
||||
}
|
||||
|
||||
Future<void> _hashTrashedAssets(LocalAlbum album, Iterable<TrashedAsset> assetsToHash) async {
|
||||
final toHash = <TrashedAsset>[];
|
||||
|
||||
for (final asset in assetsToHash) {
|
||||
if (isCancelled) {
|
||||
_log.warning("Hashing cancelled. Stopped processing assets.");
|
||||
return;
|
||||
}
|
||||
|
||||
toHash.add(asset);
|
||||
|
||||
if (toHash.length == _batchSize) {
|
||||
await _processTrashedBatch(album, toHash);
|
||||
toHash.clear();
|
||||
}
|
||||
}
|
||||
|
||||
await _processTrashedBatch(album, toHash);
|
||||
}
|
||||
|
||||
Future<void> _processTrashedBatch(LocalAlbum album, List<TrashedAsset> toHash) async {
|
||||
if (toHash.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
_log.fine("Hashing ${toHash.length} trashed files");
|
||||
|
||||
final params = toHash.map((e) => TrashedAssetParams(id: e.id, type: e.type.index, albumId: album.id)).toList();
|
||||
final hashResults = await _nativeSyncApi.hashTrashedAssets(params);
|
||||
|
||||
assert(
|
||||
hashResults.length == toHash.length,
|
||||
"Trashed Assets, Hashes length does not match toHash length: ${hashResults.length} != ${toHash.length}",
|
||||
);
|
||||
final hashed = <TrashedAsset>[];
|
||||
|
||||
for (int i = 0; i < hashResults.length; i++) {
|
||||
if (isCancelled) {
|
||||
_log.warning("Hashing cancelled. Stopped processing batch.");
|
||||
return;
|
||||
}
|
||||
|
||||
final hashResult = hashResults[i];
|
||||
final asset = toHash[i];
|
||||
if (hashResult.hash != null) {
|
||||
hashed.add(asset.copyWith(checksum: hashResult.hash!));
|
||||
} else {
|
||||
_log.warning(
|
||||
"Failed to hash trashed asset with id: ${hashResult.assetId}, name: ${asset.name}, createdAt: ${asset.createdAt}, from album: ${album.name}. Error: ${hashResult.error ?? "unknown"}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_log.fine("Hashed ${hashed.length}/${toHash.length} trashed assets");
|
||||
await _trashSyncService.updateChecksums(hashed);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ import 'package:collection/collection.dart';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/services/trash_sync.service.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/trashed_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
|
||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
||||
import 'package:immich_mobile/utils/datetime_helpers.dart';
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
|
@ -15,15 +17,18 @@ import 'package:logging/logging.dart';
|
|||
class LocalSyncService {
|
||||
final DriftLocalAlbumRepository _localAlbumRepository;
|
||||
final NativeSyncApi _nativeSyncApi;
|
||||
final TrashSyncService _trashSyncService;
|
||||
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
||||
final LocalFilesManagerRepository _localFilesManager;
|
||||
final Logger _log = Logger("DeviceSyncService");
|
||||
|
||||
LocalSyncService({
|
||||
required DriftLocalAlbumRepository localAlbumRepository,
|
||||
required TrashSyncService trashSyncService,
|
||||
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
|
||||
required LocalFilesManagerRepository localFilesManager,
|
||||
required NativeSyncApi nativeSyncApi,
|
||||
}) : _localAlbumRepository = localAlbumRepository,
|
||||
_trashSyncService = trashSyncService,
|
||||
_trashedLocalAssetRepository = trashedLocalAssetRepository,
|
||||
_localFilesManager = localFilesManager,
|
||||
_nativeSyncApi = nativeSyncApi;
|
||||
|
||||
Future<void> sync({bool full = false}) async {
|
||||
|
|
@ -34,6 +39,12 @@ class LocalSyncService {
|
|||
return await fullSync();
|
||||
}
|
||||
|
||||
if (CurrentPlatform.isAndroid) {
|
||||
final delta = await _nativeSyncApi.getMediaChanges(isTrashed: true);
|
||||
_log.fine("Delta updated in trash: ${delta.updates.length - delta.updates.length}");
|
||||
await _applyTrashDelta(delta);
|
||||
}
|
||||
|
||||
final delta = await _nativeSyncApi.getMediaChanges();
|
||||
if (!delta.hasChanges) {
|
||||
_log.fine("No media changes detected. Skipping sync");
|
||||
|
|
@ -75,11 +86,6 @@ class LocalSyncService {
|
|||
await updateAlbum(dbAlbum, album);
|
||||
}
|
||||
}
|
||||
if (_trashSyncService.isAutoSyncMode) {
|
||||
final delta = await _nativeSyncApi.getMediaChanges(isTrashed: true);
|
||||
_log.fine("Delta updated in trash: ${delta.updates.length - delta.updates.length}");
|
||||
await _trashSyncService.applyTrashDelta(delta);
|
||||
}
|
||||
await _nativeSyncApi.checkpointSync();
|
||||
} catch (e, s) {
|
||||
_log.severe("Error performing device sync", e, s);
|
||||
|
|
@ -93,6 +99,10 @@ class LocalSyncService {
|
|||
try {
|
||||
final Stopwatch stopwatch = Stopwatch()..start();
|
||||
|
||||
if (CurrentPlatform.isAndroid) {
|
||||
await _syncDeviceTrashSnapshot();
|
||||
}
|
||||
|
||||
final deviceAlbums = await _nativeSyncApi.getAlbums();
|
||||
final dbAlbums = await _localAlbumRepository.getAll(sortBy: {SortLocalAlbumsBy.id});
|
||||
|
||||
|
|
@ -104,9 +114,6 @@ class LocalSyncService {
|
|||
onlyFirst: removeAlbum,
|
||||
onlySecond: addAlbum,
|
||||
);
|
||||
if (_trashSyncService.isAutoSyncMode) {
|
||||
await _trashSyncService.syncDeviceTrashSnapshot();
|
||||
}
|
||||
|
||||
await _nativeSyncApi.checkpointSync();
|
||||
stopwatch.stop();
|
||||
|
|
@ -286,6 +293,53 @@ class LocalSyncService {
|
|||
bool _albumsEqual(LocalAlbum a, LocalAlbum b) {
|
||||
return a.name == b.name && a.assetCount == b.assetCount && a.updatedAt.isAtSameMomentAs(b.updatedAt);
|
||||
}
|
||||
|
||||
Future<void> _applyTrashDelta(SyncDelta delta) async {
|
||||
final trashUpdates = delta.updates;
|
||||
if (trashUpdates.isEmpty) {
|
||||
return Future.value();
|
||||
}
|
||||
final trashedAssets = <TrashedAsset>[];
|
||||
//todo try to reuse exist checksums from local assets table before they updated
|
||||
for (final update in trashUpdates) {
|
||||
final albums = delta.assetAlbums.cast<String, List<Object?>>();
|
||||
for (final String id in albums[update.id]!.cast<String?>().nonNulls) {
|
||||
trashedAssets.add(update.toTrashedAsset(id));
|
||||
}
|
||||
}
|
||||
_log.info("updateLocalTrashChanges trashedAssets: ${trashedAssets.map((e) => e.id)}");
|
||||
await _trashedLocalAssetRepository.saveTrashedAssets(trashedAssets);
|
||||
await _applyRemoteRestoreToLocal();
|
||||
}
|
||||
|
||||
Future<void> _syncDeviceTrashSnapshot() async {
|
||||
final backupAlbums = await _localAlbumRepository.getBackupAlbums();
|
||||
if (backupAlbums.isEmpty) {
|
||||
_log.info("syncDeviceTrashSnapshot, No backup albums found");
|
||||
return;
|
||||
}
|
||||
for (final album in backupAlbums) {
|
||||
_log.info("syncDeviceTrashSnapshot prepare, album: ${album.id}/${album.name}");
|
||||
final trashedPlatformAssets = await _nativeSyncApi.getTrashedAssetsForAlbum(album.id);
|
||||
final trashedAssets = trashedPlatformAssets.toTrashedAssets(album.id);
|
||||
await _trashedLocalAssetRepository.applyTrashSnapshot(trashedAssets, album.id);
|
||||
}
|
||||
await _applyRemoteRestoreToLocal();
|
||||
}
|
||||
|
||||
Future<void> _applyRemoteRestoreToLocal() async {
|
||||
final remoteAssetsToRestore = await _trashedLocalAssetRepository.getToRestore();
|
||||
if (remoteAssetsToRestore.isNotEmpty) {
|
||||
_log.info("remoteAssetsToRestore: $remoteAssetsToRestore");
|
||||
for (final asset in remoteAssetsToRestore) {
|
||||
_log.info("Restoring from trash, localId: ${asset.id}, remoteId: ${asset.checksum}");
|
||||
await _localFilesManager.restoreFromTrashById(asset.id, asset.type.index);
|
||||
}
|
||||
await _trashedLocalAssetRepository.restoreLocalAssets(remoteAssetsToRestore.map((e) => e.id));
|
||||
} else {
|
||||
_log.info("No remote assets found for restoration");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension on Iterable<PlatformAlbum> {
|
||||
|
|
@ -320,3 +374,26 @@ extension on Iterable<PlatformAsset> {
|
|||
).toList();
|
||||
}
|
||||
}
|
||||
|
||||
extension on PlatformAsset {
|
||||
TrashedAsset toTrashedAsset(String albumId) => TrashedAsset(
|
||||
id: id,
|
||||
name: name,
|
||||
checksum: null,
|
||||
type: AssetType.values.elementAtOrNull(type) ?? AssetType.other,
|
||||
createdAt: tryFromSecondsSinceEpoch(createdAt) ?? DateTime.now(),
|
||||
updatedAt: tryFromSecondsSinceEpoch(updatedAt) ?? DateTime.now(),
|
||||
albumId: albumId,
|
||||
width: width,
|
||||
height: height,
|
||||
durationInSeconds: durationInSeconds,
|
||||
isFavorite: isFavorite,
|
||||
orientation: orientation,
|
||||
);
|
||||
}
|
||||
|
||||
extension PlatformAssetsExtension on Iterable<PlatformAsset> {
|
||||
Iterable<TrashedAsset> toTrashedAssets(String albumId) {
|
||||
return map((e) => e.toTrashedAsset(albumId));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ class SyncStreamService {
|
|||
return _syncStreamRepository.deletePartnerV1(data.cast());
|
||||
case SyncEntityType.assetV1:
|
||||
final remoteSyncAssets = data.cast<SyncAssetV1>();
|
||||
if (_trashSyncService.isAutoSyncMode) {
|
||||
if (_trashSyncService.isTrashSyncMode) {
|
||||
await _trashSyncService.handleRemoteTrashed(
|
||||
remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.checksum),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,23 +1,14 @@
|
|||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/trashed_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
|
||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/utils/datetime_helpers.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
typedef TrashSyncItem = ({String remoteId, String checksum, DateTime? deletedAt});
|
||||
|
||||
class TrashSyncService {
|
||||
final AppSettingsService _appSettingsService;
|
||||
final NativeSyncApi _nativeSyncApi;
|
||||
final DriftLocalAssetRepository _localAssetRepository;
|
||||
final DriftLocalAlbumRepository _localAlbumRepository;
|
||||
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
||||
final LocalFilesManagerRepository _localFilesManager;
|
||||
final StorageRepository _storageRepository;
|
||||
|
|
@ -25,66 +16,25 @@ class TrashSyncService {
|
|||
|
||||
TrashSyncService({
|
||||
required AppSettingsService appSettingsService,
|
||||
required NativeSyncApi nativeSyncApi,
|
||||
required DriftLocalAssetRepository localAssetRepository,
|
||||
required DriftLocalAlbumRepository localAlbumRepository,
|
||||
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
|
||||
required LocalFilesManagerRepository localFilesManager,
|
||||
required StorageRepository storageRepository,
|
||||
}) : _appSettingsService = appSettingsService,
|
||||
_nativeSyncApi = nativeSyncApi,
|
||||
_localAssetRepository = localAssetRepository,
|
||||
_localAlbumRepository = localAlbumRepository,
|
||||
_trashedLocalAssetRepository = trashedLocalAssetRepository,
|
||||
_localFilesManager = localFilesManager,
|
||||
_storageRepository = storageRepository;
|
||||
|
||||
bool get isAutoSyncMode =>
|
||||
bool get isTrashSyncMode =>
|
||||
CurrentPlatform.isAndroid && _appSettingsService.getSetting<bool>(AppSettingsEnum.manageLocalMediaAndroid);
|
||||
|
||||
Future<void> updateChecksums(Iterable<TrashedAsset> assets) async =>
|
||||
_trashedLocalAssetRepository.updateChecksums(assets);
|
||||
|
||||
Future<Iterable<TrashedAsset>> getAssetsToHash(Iterable<String> albumIds) async =>
|
||||
_trashedLocalAssetRepository.getToHash(albumIds);
|
||||
|
||||
Future<void> syncDeviceTrashSnapshot() async {
|
||||
final backupAlbums = await _localAlbumRepository.getBackupAlbums();
|
||||
if (backupAlbums.isEmpty) {
|
||||
_logger.info("No backup albums found");
|
||||
return;
|
||||
}
|
||||
for (final album in backupAlbums) {
|
||||
_logger.info("deviceTrashedAssets prepare, album: ${album.id}/${album.name}");
|
||||
final trashedPlatformAssets = await _nativeSyncApi.getTrashedAssetsForAlbum(album.id);
|
||||
final trashedAssets = trashedPlatformAssets.toTrashedAssets(album.id);
|
||||
await _trashedLocalAssetRepository.applyTrashSnapshot(trashedAssets, album.id);
|
||||
}
|
||||
await _applyRemoteRestoreToLocal();
|
||||
}
|
||||
|
||||
Future<void> applyTrashDelta(SyncDelta delta) async {
|
||||
final trashUpdates = delta.updates;
|
||||
if (trashUpdates.isEmpty) {
|
||||
return Future.value();
|
||||
}
|
||||
final trashedAssets = <TrashedAsset>[];
|
||||
for (final update in trashUpdates) {
|
||||
final albums = delta.assetAlbums.cast<String, List<Object?>>();
|
||||
for (final String id in albums[update.id]!.cast<String?>().nonNulls) {
|
||||
trashedAssets.add(update.toTrashedAsset(id));
|
||||
}
|
||||
}
|
||||
_logger.info("updateLocalTrashChanges trashedAssets: ${trashedAssets.map((e) => e.id)}");
|
||||
await _trashedLocalAssetRepository.saveTrashedAssets(trashedAssets);
|
||||
await _applyRemoteRestoreToLocal();
|
||||
}
|
||||
|
||||
Future<void> handleRemoteTrashed(Iterable<String> checksums) async {
|
||||
if (checksums.isEmpty) {
|
||||
return Future.value();
|
||||
} else {
|
||||
final localAssetsToTrash = await _localAssetRepository.getBackupSelectedAssetsByAlbum(checksums);
|
||||
final localAssetsToTrash = await _localAssetRepository.getAssetsFromBackupAlbums(checksums);
|
||||
if (localAssetsToTrash.isNotEmpty) {
|
||||
final mediaUrls = await Future.wait(
|
||||
localAssetsToTrash.values
|
||||
|
|
@ -101,42 +51,5 @@ class TrashSyncService {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _applyRemoteRestoreToLocal() async {
|
||||
final remoteAssetsToRestore = await _trashedLocalAssetRepository.getToRestore();
|
||||
if (remoteAssetsToRestore.isNotEmpty) {
|
||||
_logger.info("remoteAssetsToRestore: $remoteAssetsToRestore");
|
||||
for (final asset in remoteAssetsToRestore) {
|
||||
_logger.info("Restoring from trash, localId: ${asset.id}, remoteId: ${asset.checksum}");
|
||||
await _localFilesManager.restoreFromTrashById(asset.id, asset.type.index);
|
||||
}
|
||||
await _trashedLocalAssetRepository.restoreLocalAssets(remoteAssetsToRestore.map((e) => e.id));
|
||||
} else {
|
||||
_logger.info("No remote assets found for restoration");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension on PlatformAsset {
|
||||
TrashedAsset toTrashedAsset(String albumId) => TrashedAsset(
|
||||
id: id,
|
||||
name: name,
|
||||
checksum: null,
|
||||
type: AssetType.values.elementAtOrNull(type) ?? AssetType.other,
|
||||
createdAt: tryFromSecondsSinceEpoch(createdAt) ?? DateTime.now(),
|
||||
updatedAt: tryFromSecondsSinceEpoch(updatedAt) ?? DateTime.now(),
|
||||
volume: volume,
|
||||
albumId: albumId,
|
||||
width: width,
|
||||
height: height,
|
||||
durationInSeconds: durationInSeconds,
|
||||
isFavorite: isFavorite,
|
||||
orientation: orientation,
|
||||
);
|
||||
}
|
||||
|
||||
extension PlatformAssetsExtension on Iterable<PlatformAsset> {
|
||||
Iterable<TrashedAsset> toTrashedAssets(String albumId) {
|
||||
return map((e) => e.toTrashedAsset(albumId));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ class TrashedLocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntity
|
|||
|
||||
TextColumn get albumId => text()();
|
||||
|
||||
TextColumn get volume => text().nullable()();
|
||||
|
||||
TextColumn get checksum => text().nullable()();
|
||||
|
||||
BoolColumn get isFavorite => boolean().withDefault(const Constant(false))();
|
||||
|
|
@ -28,7 +26,6 @@ extension TrashedLocalAssetEntityDataDomainExtension on TrashedLocalAssetEntityD
|
|||
TrashedAsset toDto() => TrashedAsset(
|
||||
id: id,
|
||||
name: name,
|
||||
volume: volume,
|
||||
albumId: albumId,
|
||||
checksum: checksum,
|
||||
type: type,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
// dart format width=80
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'
|
||||
as i1;
|
||||
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as i2;
|
||||
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.dart'
|
||||
as i3;
|
||||
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
|
||||
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'
|
||||
as i1;
|
||||
|
||||
typedef $$TrashedLocalAssetEntityTableCreateCompanionBuilder =
|
||||
i1.TrashedLocalAssetEntityCompanion Function({
|
||||
|
|
@ -19,7 +19,6 @@ typedef $$TrashedLocalAssetEntityTableCreateCompanionBuilder =
|
|||
i0.Value<int?> durationInSeconds,
|
||||
required String id,
|
||||
required String albumId,
|
||||
i0.Value<String?> volume,
|
||||
i0.Value<String?> checksum,
|
||||
i0.Value<bool> isFavorite,
|
||||
i0.Value<int> orientation,
|
||||
|
|
@ -35,7 +34,6 @@ typedef $$TrashedLocalAssetEntityTableUpdateCompanionBuilder =
|
|||
i0.Value<int?> durationInSeconds,
|
||||
i0.Value<String> id,
|
||||
i0.Value<String> albumId,
|
||||
i0.Value<String?> volume,
|
||||
i0.Value<String?> checksum,
|
||||
i0.Value<bool> isFavorite,
|
||||
i0.Value<int> orientation,
|
||||
|
|
@ -97,11 +95,6 @@ class $$TrashedLocalAssetEntityTableFilterComposer
|
|||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<String> get volume => $composableBuilder(
|
||||
column: $table.volume,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnFilters<String> get checksum => $composableBuilder(
|
||||
column: $table.checksum,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
|
|
@ -173,11 +166,6 @@ class $$TrashedLocalAssetEntityTableOrderingComposer
|
|||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<String> get volume => $composableBuilder(
|
||||
column: $table.volume,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<String> get checksum => $composableBuilder(
|
||||
column: $table.checksum,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
|
|
@ -233,9 +221,6 @@ class $$TrashedLocalAssetEntityTableAnnotationComposer
|
|||
i0.GeneratedColumn<String> get albumId =>
|
||||
$composableBuilder(column: $table.albumId, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<String> get volume =>
|
||||
$composableBuilder(column: $table.volume, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumn<String> get checksum =>
|
||||
$composableBuilder(column: $table.checksum, builder: (column) => column);
|
||||
|
||||
|
|
@ -305,7 +290,6 @@ class $$TrashedLocalAssetEntityTableTableManager
|
|||
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
|
||||
i0.Value<String> id = const i0.Value.absent(),
|
||||
i0.Value<String> albumId = const i0.Value.absent(),
|
||||
i0.Value<String?> volume = const i0.Value.absent(),
|
||||
i0.Value<String?> checksum = const i0.Value.absent(),
|
||||
i0.Value<bool> isFavorite = const i0.Value.absent(),
|
||||
i0.Value<int> orientation = const i0.Value.absent(),
|
||||
|
|
@ -319,7 +303,6 @@ class $$TrashedLocalAssetEntityTableTableManager
|
|||
durationInSeconds: durationInSeconds,
|
||||
id: id,
|
||||
albumId: albumId,
|
||||
volume: volume,
|
||||
checksum: checksum,
|
||||
isFavorite: isFavorite,
|
||||
orientation: orientation,
|
||||
|
|
@ -335,7 +318,6 @@ class $$TrashedLocalAssetEntityTableTableManager
|
|||
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
|
||||
required String id,
|
||||
required String albumId,
|
||||
i0.Value<String?> volume = const i0.Value.absent(),
|
||||
i0.Value<String?> checksum = const i0.Value.absent(),
|
||||
i0.Value<bool> isFavorite = const i0.Value.absent(),
|
||||
i0.Value<int> orientation = const i0.Value.absent(),
|
||||
|
|
@ -349,7 +331,6 @@ class $$TrashedLocalAssetEntityTableTableManager
|
|||
durationInSeconds: durationInSeconds,
|
||||
id: id,
|
||||
albumId: albumId,
|
||||
volume: volume,
|
||||
checksum: checksum,
|
||||
isFavorite: isFavorite,
|
||||
orientation: orientation,
|
||||
|
|
@ -499,17 +480,6 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
|
|||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
);
|
||||
static const i0.VerificationMeta _volumeMeta = const i0.VerificationMeta(
|
||||
'volume',
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> volume = i0.GeneratedColumn<String>(
|
||||
'volume',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
static const i0.VerificationMeta _checksumMeta = const i0.VerificationMeta(
|
||||
'checksum',
|
||||
);
|
||||
|
|
@ -559,7 +529,6 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
|
|||
durationInSeconds,
|
||||
id,
|
||||
albumId,
|
||||
volume,
|
||||
checksum,
|
||||
isFavorite,
|
||||
orientation,
|
||||
|
|
@ -630,12 +599,6 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
|
|||
} else if (isInserting) {
|
||||
context.missing(_albumIdMeta);
|
||||
}
|
||||
if (data.containsKey('volume')) {
|
||||
context.handle(
|
||||
_volumeMeta,
|
||||
volume.isAcceptableOrUnknown(data['volume']!, _volumeMeta),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('checksum')) {
|
||||
context.handle(
|
||||
_checksumMeta,
|
||||
|
|
@ -707,10 +670,6 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
|
|||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}album_id'],
|
||||
)!,
|
||||
volume: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}volume'],
|
||||
),
|
||||
checksum: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}checksum'],
|
||||
|
|
@ -750,7 +709,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
|||
final int? durationInSeconds;
|
||||
final String id;
|
||||
final String albumId;
|
||||
final String? volume;
|
||||
final String? checksum;
|
||||
final bool isFavorite;
|
||||
final int orientation;
|
||||
|
|
@ -764,7 +722,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
|||
this.durationInSeconds,
|
||||
required this.id,
|
||||
required this.albumId,
|
||||
this.volume,
|
||||
this.checksum,
|
||||
required this.isFavorite,
|
||||
required this.orientation,
|
||||
|
|
@ -791,9 +748,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
|||
}
|
||||
map['id'] = i0.Variable<String>(id);
|
||||
map['album_id'] = i0.Variable<String>(albumId);
|
||||
if (!nullToAbsent || volume != null) {
|
||||
map['volume'] = i0.Variable<String>(volume);
|
||||
}
|
||||
if (!nullToAbsent || checksum != null) {
|
||||
map['checksum'] = i0.Variable<String>(checksum);
|
||||
}
|
||||
|
|
@ -819,7 +773,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
|||
durationInSeconds: serializer.fromJson<int?>(json['durationInSeconds']),
|
||||
id: serializer.fromJson<String>(json['id']),
|
||||
albumId: serializer.fromJson<String>(json['albumId']),
|
||||
volume: serializer.fromJson<String?>(json['volume']),
|
||||
checksum: serializer.fromJson<String?>(json['checksum']),
|
||||
isFavorite: serializer.fromJson<bool>(json['isFavorite']),
|
||||
orientation: serializer.fromJson<int>(json['orientation']),
|
||||
|
|
@ -840,7 +793,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
|||
'durationInSeconds': serializer.toJson<int?>(durationInSeconds),
|
||||
'id': serializer.toJson<String>(id),
|
||||
'albumId': serializer.toJson<String>(albumId),
|
||||
'volume': serializer.toJson<String?>(volume),
|
||||
'checksum': serializer.toJson<String?>(checksum),
|
||||
'isFavorite': serializer.toJson<bool>(isFavorite),
|
||||
'orientation': serializer.toJson<int>(orientation),
|
||||
|
|
@ -857,7 +809,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
|||
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
|
||||
String? id,
|
||||
String? albumId,
|
||||
i0.Value<String?> volume = const i0.Value.absent(),
|
||||
i0.Value<String?> checksum = const i0.Value.absent(),
|
||||
bool? isFavorite,
|
||||
int? orientation,
|
||||
|
|
@ -873,7 +824,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
|||
: this.durationInSeconds,
|
||||
id: id ?? this.id,
|
||||
albumId: albumId ?? this.albumId,
|
||||
volume: volume.present ? volume.value : this.volume,
|
||||
checksum: checksum.present ? checksum.value : this.checksum,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
orientation: orientation ?? this.orientation,
|
||||
|
|
@ -893,7 +843,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
|||
: this.durationInSeconds,
|
||||
id: data.id.present ? data.id.value : this.id,
|
||||
albumId: data.albumId.present ? data.albumId.value : this.albumId,
|
||||
volume: data.volume.present ? data.volume.value : this.volume,
|
||||
checksum: data.checksum.present ? data.checksum.value : this.checksum,
|
||||
isFavorite: data.isFavorite.present
|
||||
? data.isFavorite.value
|
||||
|
|
@ -916,7 +865,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
|||
..write('durationInSeconds: $durationInSeconds, ')
|
||||
..write('id: $id, ')
|
||||
..write('albumId: $albumId, ')
|
||||
..write('volume: $volume, ')
|
||||
..write('checksum: $checksum, ')
|
||||
..write('isFavorite: $isFavorite, ')
|
||||
..write('orientation: $orientation')
|
||||
|
|
@ -935,7 +883,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
|||
durationInSeconds,
|
||||
id,
|
||||
albumId,
|
||||
volume,
|
||||
checksum,
|
||||
isFavorite,
|
||||
orientation,
|
||||
|
|
@ -953,7 +900,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
|||
other.durationInSeconds == this.durationInSeconds &&
|
||||
other.id == this.id &&
|
||||
other.albumId == this.albumId &&
|
||||
other.volume == this.volume &&
|
||||
other.checksum == this.checksum &&
|
||||
other.isFavorite == this.isFavorite &&
|
||||
other.orientation == this.orientation);
|
||||
|
|
@ -970,7 +916,6 @@ class TrashedLocalAssetEntityCompanion
|
|||
final i0.Value<int?> durationInSeconds;
|
||||
final i0.Value<String> id;
|
||||
final i0.Value<String> albumId;
|
||||
final i0.Value<String?> volume;
|
||||
final i0.Value<String?> checksum;
|
||||
final i0.Value<bool> isFavorite;
|
||||
final i0.Value<int> orientation;
|
||||
|
|
@ -984,7 +929,6 @@ class TrashedLocalAssetEntityCompanion
|
|||
this.durationInSeconds = const i0.Value.absent(),
|
||||
this.id = const i0.Value.absent(),
|
||||
this.albumId = const i0.Value.absent(),
|
||||
this.volume = const i0.Value.absent(),
|
||||
this.checksum = const i0.Value.absent(),
|
||||
this.isFavorite = const i0.Value.absent(),
|
||||
this.orientation = const i0.Value.absent(),
|
||||
|
|
@ -999,7 +943,6 @@ class TrashedLocalAssetEntityCompanion
|
|||
this.durationInSeconds = const i0.Value.absent(),
|
||||
required String id,
|
||||
required String albumId,
|
||||
this.volume = const i0.Value.absent(),
|
||||
this.checksum = const i0.Value.absent(),
|
||||
this.isFavorite = const i0.Value.absent(),
|
||||
this.orientation = const i0.Value.absent(),
|
||||
|
|
@ -1017,7 +960,6 @@ class TrashedLocalAssetEntityCompanion
|
|||
i0.Expression<int>? durationInSeconds,
|
||||
i0.Expression<String>? id,
|
||||
i0.Expression<String>? albumId,
|
||||
i0.Expression<String>? volume,
|
||||
i0.Expression<String>? checksum,
|
||||
i0.Expression<bool>? isFavorite,
|
||||
i0.Expression<int>? orientation,
|
||||
|
|
@ -1032,7 +974,6 @@ class TrashedLocalAssetEntityCompanion
|
|||
if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds,
|
||||
if (id != null) 'id': id,
|
||||
if (albumId != null) 'album_id': albumId,
|
||||
if (volume != null) 'volume': volume,
|
||||
if (checksum != null) 'checksum': checksum,
|
||||
if (isFavorite != null) 'is_favorite': isFavorite,
|
||||
if (orientation != null) 'orientation': orientation,
|
||||
|
|
@ -1049,7 +990,6 @@ class TrashedLocalAssetEntityCompanion
|
|||
i0.Value<int?>? durationInSeconds,
|
||||
i0.Value<String>? id,
|
||||
i0.Value<String>? albumId,
|
||||
i0.Value<String?>? volume,
|
||||
i0.Value<String?>? checksum,
|
||||
i0.Value<bool>? isFavorite,
|
||||
i0.Value<int>? orientation,
|
||||
|
|
@ -1064,7 +1004,6 @@ class TrashedLocalAssetEntityCompanion
|
|||
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
|
||||
id: id ?? this.id,
|
||||
albumId: albumId ?? this.albumId,
|
||||
volume: volume ?? this.volume,
|
||||
checksum: checksum ?? this.checksum,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
orientation: orientation ?? this.orientation,
|
||||
|
|
@ -1103,9 +1042,6 @@ class TrashedLocalAssetEntityCompanion
|
|||
if (albumId.present) {
|
||||
map['album_id'] = i0.Variable<String>(albumId.value);
|
||||
}
|
||||
if (volume.present) {
|
||||
map['volume'] = i0.Variable<String>(volume.value);
|
||||
}
|
||||
if (checksum.present) {
|
||||
map['checksum'] = i0.Variable<String>(checksum.value);
|
||||
}
|
||||
|
|
@ -1130,7 +1066,6 @@ class TrashedLocalAssetEntityCompanion
|
|||
..write('durationInSeconds: $durationInSeconds, ')
|
||||
..write('id: $id, ')
|
||||
..write('albumId: $albumId, ')
|
||||
..write('volume: $volume, ')
|
||||
..write('checksum: $checksum, ')
|
||||
..write('isFavorite: $isFavorite, ')
|
||||
..write('orientation: $orientation')
|
||||
|
|
|
|||
|
|
@ -100,26 +100,27 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
|||
return query.map((localAlbum) => localAlbum.toDto()).get();
|
||||
}
|
||||
|
||||
Future<Map<AlbumId, List<LocalAsset>>> getBackupSelectedAssetsByAlbum(Iterable<String> checksums) async {
|
||||
Future<Map<AlbumId, List<LocalAsset>>> getAssetsFromBackupAlbums(Iterable<String> checksums) async {
|
||||
if (checksums.isEmpty) {
|
||||
return {};
|
||||
}
|
||||
|
||||
final lAlbumAsset = _db.localAlbumAssetEntity;
|
||||
final lAlbum = _db.localAlbumEntity;
|
||||
final lAsset = _db.localAssetEntity;
|
||||
|
||||
final result = <String, List<LocalAsset>>{};
|
||||
|
||||
for (final slice in checksums.toSet().slices(800)) {
|
||||
final rows = await (_db.select(lAlbumAsset).join([
|
||||
innerJoin(lAlbum, lAlbumAsset.albumId.equalsExp(lAlbum.id)),
|
||||
innerJoin(lAsset, lAlbumAsset.assetId.equalsExp(lAsset.id)),
|
||||
])..where(lAlbum.backupSelection.equalsValue(BackupSelection.selected) & lAsset.checksum.isIn(slice))).get();
|
||||
for (final slice in checksums.toSet().slices(32000)) {
|
||||
final rows =
|
||||
await (_db.select(_db.localAlbumAssetEntity).join([
|
||||
innerJoin(_db.localAlbumEntity, _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id)),
|
||||
innerJoin(_db.localAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id)),
|
||||
])..where(
|
||||
_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected) &
|
||||
_db.localAssetEntity.checksum.isIn(slice),
|
||||
))
|
||||
.get();
|
||||
|
||||
for (final row in rows) {
|
||||
final albumId = row.readTable(lAlbumAsset).albumId;
|
||||
final assetData = row.readTable(lAsset);
|
||||
final albumId = row.readTable(_db.localAlbumAssetEntity).albumId;
|
||||
final assetData = row.readTable(_db.localAssetEntity);
|
||||
final asset = assetData.toDto();
|
||||
(result[albumId] ??= <LocalAsset>[]).add(asset);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,41 +14,45 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
|||
|
||||
const DriftTrashedLocalAssetRepository(this._db) : super(_db);
|
||||
|
||||
Future<void> updateChecksums(Iterable<TrashedAsset> assets) {
|
||||
if (assets.isEmpty) {
|
||||
Future<void> updateHashes(Map<String, String> hashes) {
|
||||
if (hashes.isEmpty) {
|
||||
return Future.value();
|
||||
}
|
||||
final now = DateTime.now();
|
||||
return _db.batch((batch) async {
|
||||
for (final asset in assets) {
|
||||
for (final entry in hashes.entries) {
|
||||
batch.update(
|
||||
_db.trashedLocalAssetEntity,
|
||||
TrashedLocalAssetEntityCompanion(checksum: Value(asset.checksum), updatedAt: Value(now)),
|
||||
where: (e) => e.id.equals(asset.id),
|
||||
TrashedLocalAssetEntityCompanion(checksum: Value(entry.value), updatedAt: Value(now)),
|
||||
where: (e) => e.id.equals(entry.key),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<Iterable<TrashedAsset>> getToHash(Iterable<String> albumIds) {
|
||||
Future<Iterable<TrashedAsset>> getAssetsToHash(Iterable<String> albumIds) {
|
||||
final query = _db.trashedLocalAssetEntity.select()..where((r) => r.albumId.isIn(albumIds) & r.checksum.isNull());
|
||||
return query.map((row) => row.toDto()).get();
|
||||
}
|
||||
|
||||
Future<Iterable<TrashedAsset>> getToRestore() async {
|
||||
final trashed = _db.trashedLocalAssetEntity;
|
||||
final remote = _db.remoteAssetEntity;
|
||||
final album = _db.localAlbumEntity;
|
||||
final selectedAlbumIds = (_db.selectOnly(_db.localAlbumEntity)
|
||||
..addColumns([_db.localAlbumEntity.id])
|
||||
..where(_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected)));
|
||||
|
||||
final selectedAlbumIds = (_db.selectOnly(album)
|
||||
..addColumns([album.id])
|
||||
..where(album.backupSelection.equalsValue(BackupSelection.selected)));
|
||||
final rows =
|
||||
await (_db.select(_db.trashedLocalAssetEntity).join([
|
||||
innerJoin(
|
||||
_db.remoteAssetEntity,
|
||||
_db.remoteAssetEntity.checksum.equalsExp(_db.trashedLocalAssetEntity.checksum),
|
||||
),
|
||||
])..where(
|
||||
_db.trashedLocalAssetEntity.albumId.isInQuery(selectedAlbumIds) &
|
||||
_db.remoteAssetEntity.deletedAt.isNull(),
|
||||
))
|
||||
.get();
|
||||
|
||||
final rows = await (_db.select(trashed).join([
|
||||
innerJoin(remote, remote.checksum.equalsExp(trashed.checksum)),
|
||||
])..where(trashed.albumId.isInQuery(selectedAlbumIds) & remote.deletedAt.isNull())).get();
|
||||
|
||||
return rows.map((result) => result.readTable(trashed).toDto());
|
||||
return rows.map((result) => result.readTable(_db.trashedLocalAssetEntity).toDto());
|
||||
}
|
||||
|
||||
/// Applies resulted snapshot of trashed assets:
|
||||
|
|
@ -61,41 +65,46 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
|||
}
|
||||
|
||||
return _db.transaction(() async {
|
||||
final table = _db.trashedLocalAssetEntity;
|
||||
|
||||
final companions = assets.map(
|
||||
(a) => TrashedLocalAssetEntityCompanion.insert(
|
||||
id: a.id,
|
||||
await _db.batch((batch) {
|
||||
for (final asset in assets) {
|
||||
final companion = TrashedLocalAssetEntityCompanion.insert(
|
||||
id: asset.id,
|
||||
albumId: albumId,
|
||||
volume: a.volume == null ? const Value.absent() : Value(a.volume),
|
||||
checksum: a.checksum == null ? const Value.absent() : Value(a.checksum),
|
||||
name: a.name,
|
||||
type: a.type,
|
||||
createdAt: Value(a.createdAt),
|
||||
updatedAt: Value(a.updatedAt),
|
||||
width: Value(a.width),
|
||||
height: Value(a.height),
|
||||
durationInSeconds: Value(a.durationInSeconds),
|
||||
isFavorite: Value(a.isFavorite),
|
||||
orientation: Value(a.orientation),
|
||||
),
|
||||
checksum: asset.checksum == null ? const Value.absent() : Value(asset.checksum),
|
||||
name: asset.name,
|
||||
type: asset.type,
|
||||
createdAt: Value(asset.createdAt),
|
||||
updatedAt: Value(asset.updatedAt),
|
||||
width: Value(asset.width),
|
||||
height: Value(asset.height),
|
||||
durationInSeconds: Value(asset.durationInSeconds),
|
||||
isFavorite: Value(asset.isFavorite),
|
||||
orientation: Value(asset.orientation),
|
||||
);
|
||||
|
||||
for (final slice in companions.slices(400)) {
|
||||
await _db.batch((b) {
|
||||
b.insertAllOnConflictUpdate(table, slice);
|
||||
});
|
||||
batch.insert<$TrashedLocalAssetEntityTable, TrashedLocalAssetEntityData>(
|
||||
_db.trashedLocalAssetEntity,
|
||||
companion,
|
||||
onConflict: DoUpdate((_) => companion, where: (old) => old.updatedAt.isNotValue(asset.updatedAt)),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
final keepIds = assets.map((asset) => asset.id);
|
||||
if (keepIds.length <= 900) {
|
||||
await (_db.delete(table)..where((row) => row.id.isNotIn(keepIds))).go();
|
||||
|
||||
if (keepIds.length <= 32000) {
|
||||
await (_db.delete(_db.trashedLocalAssetEntity)..where((row) => row.id.isNotIn(keepIds))).go();
|
||||
} else {
|
||||
final existingIds = await (_db.selectOnly(table)..addColumns([table.id])).map((r) => r.read(table.id)!).get();
|
||||
final toDelete = existingIds.where((id) => !keepIds.contains(id));
|
||||
for (final slice in toDelete.slices(400)) {
|
||||
await (_db.delete(table)..where((row) => row.id.isIn(slice))).go();
|
||||
final keepIdsSet = keepIds.toSet();
|
||||
final existingIds = await (_db.selectOnly(
|
||||
_db.trashedLocalAssetEntity,
|
||||
)..addColumns([_db.trashedLocalAssetEntity.id])).map((r) => r.read(_db.trashedLocalAssetEntity.id)!).get();
|
||||
final idToDelete = existingIds.where((id) => !keepIdsSet.contains(id));
|
||||
await _db.batch((batch) {
|
||||
for (final id in idToDelete) {
|
||||
batch.deleteWhere(_db.trashedLocalAssetEntity, (row) => row.id.equals(id));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -104,42 +113,43 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
|||
if (trashUpdates.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final companions = trashUpdates.map(
|
||||
(a) => TrashedLocalAssetEntityCompanion.insert(
|
||||
id: a.id,
|
||||
volume: a.volume == null ? const Value.absent() : Value(a.volume),
|
||||
albumId: a.albumId,
|
||||
name: a.name,
|
||||
type: a.type,
|
||||
checksum: a.checksum == null ? const Value.absent() : Value(a.checksum),
|
||||
createdAt: Value(a.createdAt),
|
||||
width: Value(a.width),
|
||||
height: Value(a.height),
|
||||
durationInSeconds: Value(a.durationInSeconds),
|
||||
isFavorite: Value(a.isFavorite),
|
||||
orientation: Value(a.orientation),
|
||||
),
|
||||
);
|
||||
|
||||
for (final slice in companions.slices(200)) {
|
||||
await _db.batch((b) {
|
||||
b.insertAllOnConflictUpdate(_db.trashedLocalAssetEntity, slice);
|
||||
});
|
||||
await _db.batch((batch) {
|
||||
for (final asset in trashUpdates) {
|
||||
final companion = TrashedLocalAssetEntityCompanion.insert(
|
||||
id: asset.id,
|
||||
albumId: asset.albumId,
|
||||
name: asset.name,
|
||||
type: asset.type,
|
||||
checksum: asset.checksum == null ? const Value.absent() : Value(asset.checksum),
|
||||
createdAt: Value(asset.createdAt),
|
||||
width: Value(asset.width),
|
||||
height: Value(asset.height),
|
||||
durationInSeconds: Value(asset.durationInSeconds),
|
||||
isFavorite: Value(asset.isFavorite),
|
||||
orientation: Value(asset.orientation),
|
||||
);
|
||||
batch.insert<$TrashedLocalAssetEntityTable, TrashedLocalAssetEntityData>(
|
||||
_db.trashedLocalAssetEntity,
|
||||
companion,
|
||||
onConflict: DoUpdate((_) => companion, where: (old) => old.updatedAt.isNotValue(asset.updatedAt)),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Stream<int> watchCount() {
|
||||
final t = _db.trashedLocalAssetEntity;
|
||||
return (_db.selectOnly(t)..addColumns([t.id.count()])).watchSingle().map((row) => row.read<int>(t.id.count()) ?? 0);
|
||||
return (_db.selectOnly(_db.trashedLocalAssetEntity)..addColumns([_db.trashedLocalAssetEntity.id.count()]))
|
||||
.watchSingle()
|
||||
.map((row) => row.read<int>(_db.trashedLocalAssetEntity.id.count()) ?? 0);
|
||||
}
|
||||
|
||||
Stream<int> watchHashedCount() {
|
||||
final t = _db.trashedLocalAssetEntity;
|
||||
return (_db.selectOnly(t)
|
||||
..addColumns([t.id.count()])
|
||||
..where(t.checksum.isNotNull()))
|
||||
return (_db.selectOnly(_db.trashedLocalAssetEntity)
|
||||
..addColumns([_db.trashedLocalAssetEntity.id.count()])
|
||||
..where(_db.trashedLocalAssetEntity.checksum.isNotNull()))
|
||||
.watchSingle()
|
||||
.map((row) => row.read<int>(t.id.count()) ?? 0);
|
||||
.map((row) => row.read<int>(_db.trashedLocalAssetEntity.id.count()) ?? 0);
|
||||
}
|
||||
|
||||
Future<void> trashLocalAsset(Map<AlbumId, List<LocalAsset>> assetsByAlbums) async {
|
||||
|
|
@ -150,14 +160,14 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
|||
final companions = <TrashedLocalAssetEntityCompanion>[];
|
||||
final idToDelete = <String>{};
|
||||
|
||||
assetsByAlbums.forEach((albumId, assets) {
|
||||
for (final asset in assets) {
|
||||
for (final entry in assetsByAlbums.entries) {
|
||||
for (final asset in entry.value) {
|
||||
idToDelete.add(asset.id);
|
||||
companions.add(
|
||||
TrashedLocalAssetEntityCompanion(
|
||||
id: Value(asset.id),
|
||||
name: Value(asset.name),
|
||||
albumId: Value(albumId),
|
||||
albumId: Value(entry.key),
|
||||
checksum: asset.checksum == null ? const Value.absent() : Value(asset.checksum),
|
||||
type: Value(asset.type),
|
||||
width: Value(asset.width),
|
||||
|
|
@ -168,17 +178,18 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
|||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await _db.transaction(() async {
|
||||
for (final slice in companions.slices(200)) {
|
||||
await _db.batch((batch) {
|
||||
for (final slice in companions.slices(32000)) {
|
||||
batch.insertAllOnConflictUpdate(_db.trashedLocalAssetEntity, slice);
|
||||
}
|
||||
|
||||
for (final id in idToDelete) {
|
||||
batch.deleteWhere(_db.localAssetEntity, (row) => row.id.equals(id));
|
||||
}
|
||||
});
|
||||
}
|
||||
for (final slice in idToDelete.slices(800)) {
|
||||
await (_db.delete(_db.localAssetEntity)..where((e) => e.id.isIn(slice))).go();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -212,8 +223,8 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
|||
await _db.transaction(() async {
|
||||
await _db.batch((batch) {
|
||||
batch.insertAllOnConflictUpdate(_db.localAssetEntity, localAssets);
|
||||
for (final slice in ids.slices(32000)) {
|
||||
batch.deleteWhere(_db.trashedLocalAssetEntity, (tbl) => tbl.id.isIn(slice));
|
||||
for (final id in ids) {
|
||||
batch.deleteWhere(_db.trashedLocalAssetEntity, (row) => row.id.equals(id));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
95
mobile/lib/platform/native_sync_api.g.dart
generated
95
mobile/lib/platform/native_sync_api.g.dart
generated
|
|
@ -41,8 +41,6 @@ class PlatformAsset {
|
|||
required this.durationInSeconds,
|
||||
required this.orientation,
|
||||
required this.isFavorite,
|
||||
this.isTrashed,
|
||||
this.volume,
|
||||
});
|
||||
|
||||
String id;
|
||||
|
|
@ -65,25 +63,8 @@ class PlatformAsset {
|
|||
|
||||
bool isFavorite;
|
||||
|
||||
bool? isTrashed;
|
||||
|
||||
String? volume;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
width,
|
||||
height,
|
||||
durationInSeconds,
|
||||
orientation,
|
||||
isFavorite,
|
||||
isTrashed,
|
||||
volume,
|
||||
];
|
||||
return <Object?>[id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite];
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
|
|
@ -103,8 +84,6 @@ class PlatformAsset {
|
|||
durationInSeconds: result[7]! as int,
|
||||
orientation: result[8]! as int,
|
||||
isFavorite: result[9]! as bool,
|
||||
isTrashed: result[10] as bool?,
|
||||
volume: result[11] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -265,45 +244,6 @@ class HashResult {
|
|||
int get hashCode => Object.hashAll(_toList());
|
||||
}
|
||||
|
||||
class TrashedAssetParams {
|
||||
TrashedAssetParams({required this.id, required this.type, this.albumId});
|
||||
|
||||
String id;
|
||||
|
||||
int type;
|
||||
|
||||
String? albumId;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[id, type, albumId];
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList();
|
||||
}
|
||||
|
||||
static TrashedAssetParams decode(Object result) {
|
||||
result as List<Object?>;
|
||||
return TrashedAssetParams(id: result[0]! as String, type: result[1]! as int, albumId: result[2] as String?);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
bool operator ==(Object other) {
|
||||
if (other is! TrashedAssetParams || 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
|
||||
|
|
@ -323,9 +263,6 @@ class _PigeonCodec extends StandardMessageCodec {
|
|||
} else if (value is HashResult) {
|
||||
buffer.putUint8(132);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is TrashedAssetParams) {
|
||||
buffer.putUint8(133);
|
||||
writeValue(buffer, value.encode());
|
||||
} else {
|
||||
super.writeValue(buffer, value);
|
||||
}
|
||||
|
|
@ -342,8 +279,6 @@ class _PigeonCodec extends StandardMessageCodec {
|
|||
return SyncDelta.decode(readValue(buffer)!);
|
||||
case 132:
|
||||
return HashResult.decode(readValue(buffer)!);
|
||||
case 133:
|
||||
return TrashedAssetParams.decode(readValue(buffer)!);
|
||||
default:
|
||||
return super.readValueOfType(type, buffer);
|
||||
}
|
||||
|
|
@ -655,32 +590,4 @@ class NativeSyncApi {
|
|||
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<PlatformAsset>();
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<HashResult>> hashTrashedAssets(List<TrashedAssetParams> trashedAssets) async {
|
||||
final String pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashTrashedAssets$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[trashedAssets]);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else if (pigeonVar_replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<HashResult>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart';
|
|||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/trash_sync.provider.dart';
|
||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
||||
|
||||
final syncStreamServiceProvider = Provider(
|
||||
(ref) => SyncStreamService(
|
||||
|
|
@ -28,7 +29,8 @@ final syncStreamRepositoryProvider = Provider((ref) => SyncStreamRepository(ref.
|
|||
final localSyncServiceProvider = Provider(
|
||||
(ref) => LocalSyncService(
|
||||
localAlbumRepository: ref.watch(localAlbumRepository),
|
||||
trashSyncService: ref.watch(trashSyncServiceProvider),
|
||||
trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository),
|
||||
localFilesManager: ref.watch(localFilesManagerRepositoryProvider),
|
||||
nativeSyncApi: ref.watch(nativeSyncApiProvider),
|
||||
),
|
||||
);
|
||||
|
|
@ -38,6 +40,6 @@ final hashServiceProvider = Provider(
|
|||
localAlbumRepository: ref.watch(localAlbumRepository),
|
||||
localAssetRepository: ref.watch(localAssetRepository),
|
||||
nativeSyncApi: ref.watch(nativeSyncApiProvider),
|
||||
trashSyncService: ref.watch(trashSyncServiceProvider),
|
||||
trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,21 +2,16 @@ import 'package:async/async.dart';
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/services/trash_sync.service.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
|
||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
||||
|
||||
import 'asset.provider.dart';
|
||||
|
||||
typedef TrashedAssetsCount = ({int total, int hashed});
|
||||
|
||||
final trashSyncServiceProvider = Provider(
|
||||
(ref) => TrashSyncService(
|
||||
appSettingsService: ref.watch(appSettingsServiceProvider),
|
||||
nativeSyncApi: ref.watch(nativeSyncApiProvider),
|
||||
localAssetRepository: ref.watch(localAssetRepository),
|
||||
localAlbumRepository: ref.watch(localAlbumRepository),
|
||||
trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository),
|
||||
localFilesManager: ref.watch(localFilesManagerRepositoryProvider),
|
||||
storageRepository: ref.watch(storageRepositoryProvider),
|
||||
|
|
|
|||
|
|
@ -353,7 +353,7 @@ class _SyncStatsCounts extends ConsumerWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
if (trashSyncService.isAutoSyncMode) ...[
|
||||
if (trashSyncService.isTrashSyncMode) ...[
|
||||
_SectionHeaderText(text: "trash".t(context: context)),
|
||||
Consumer(
|
||||
builder: (context, ref, _) {
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@ import 'package:pigeon/pigeon.dart';
|
|||
class PlatformAsset {
|
||||
final String id;
|
||||
final String name;
|
||||
|
||||
// Follows AssetType enum from base_asset.model.dart
|
||||
final int type;
|
||||
|
||||
// Seconds since epoch
|
||||
final int? createdAt;
|
||||
final int? updatedAt;
|
||||
|
|
@ -24,8 +26,6 @@ class PlatformAsset {
|
|||
final int durationInSeconds;
|
||||
final int orientation;
|
||||
final bool isFavorite;
|
||||
final bool? isTrashed;
|
||||
final String? volume;
|
||||
|
||||
const PlatformAsset({
|
||||
required this.id,
|
||||
|
|
@ -38,14 +38,13 @@ class PlatformAsset {
|
|||
this.durationInSeconds = 0,
|
||||
this.orientation = 0,
|
||||
this.isFavorite = false,
|
||||
this.isTrashed,
|
||||
this.volume,
|
||||
});
|
||||
}
|
||||
|
||||
class PlatformAlbum {
|
||||
final String id;
|
||||
final String name;
|
||||
|
||||
// Seconds since epoch
|
||||
final int? updatedAt;
|
||||
final bool isCloud;
|
||||
|
|
@ -64,6 +63,7 @@ class SyncDelta {
|
|||
final bool hasChanges;
|
||||
final List<PlatformAsset> updates;
|
||||
final List<String> deletes;
|
||||
|
||||
// Asset -> Album mapping
|
||||
final Map<String, List<String>> assetAlbums;
|
||||
|
||||
|
|
@ -83,18 +83,6 @@ class HashResult {
|
|||
const HashResult({required this.assetId, this.error, this.hash});
|
||||
}
|
||||
|
||||
class TrashedAssetParams {
|
||||
final String id;
|
||||
final int type;
|
||||
final String? albumId;
|
||||
|
||||
const TrashedAssetParams({
|
||||
required this.id,
|
||||
required this.type,
|
||||
this.albumId,
|
||||
});
|
||||
}
|
||||
|
||||
@HostApi()
|
||||
abstract class NativeSyncApi {
|
||||
bool shouldFullSync();
|
||||
|
|
@ -126,8 +114,4 @@ abstract class NativeSyncApi {
|
|||
|
||||
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
|
||||
List<PlatformAsset> getTrashedAssetsForAlbum(String albumId);
|
||||
|
||||
@async
|
||||
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
|
||||
List<HashResult> hashTrashedAssets(List<TrashedAssetParams> trashedAssets);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,19 +14,19 @@ void main() {
|
|||
late MockLocalAlbumRepository mockAlbumRepo;
|
||||
late MockLocalAssetRepository mockAssetRepo;
|
||||
late MockNativeSyncApi mockNativeApi;
|
||||
late MockTrashSyncService mockTrashSyncService;
|
||||
late MockTrashedLocalAssetRepository mockTrashedAssetRepo;
|
||||
|
||||
setUp(() {
|
||||
mockAlbumRepo = MockLocalAlbumRepository();
|
||||
mockAssetRepo = MockLocalAssetRepository();
|
||||
mockNativeApi = MockNativeSyncApi();
|
||||
mockTrashSyncService = MockTrashSyncService();
|
||||
mockTrashedAssetRepo = MockTrashedLocalAssetRepository();
|
||||
|
||||
sut = HashService(
|
||||
localAlbumRepository: mockAlbumRepo,
|
||||
localAssetRepository: mockAssetRepo,
|
||||
nativeSyncApi: mockNativeApi,
|
||||
trashSyncService: mockTrashSyncService,
|
||||
trashedLocalAssetRepository: mockTrashedAssetRepo,
|
||||
);
|
||||
|
||||
registerFallbackValue(LocalAlbumStub.recent);
|
||||
|
|
@ -34,7 +34,6 @@ void main() {
|
|||
registerFallbackValue(<String, String>{});
|
||||
|
||||
when(() => mockAssetRepo.updateHashes(any())).thenAnswer((_) async => {});
|
||||
when(() => mockTrashSyncService.isAutoSyncMode).thenReturn(false);
|
||||
});
|
||||
|
||||
group('HashService hashAssets', () {
|
||||
|
|
@ -118,7 +117,7 @@ void main() {
|
|||
localAssetRepository: mockAssetRepo,
|
||||
nativeSyncApi: mockNativeApi,
|
||||
batchSize: batchSize,
|
||||
trashSyncService: mockTrashSyncService,
|
||||
trashedLocalAssetRepository: mockTrashedAssetRepo,
|
||||
);
|
||||
|
||||
final album = LocalAlbumStub.recent;
|
||||
|
|
@ -191,4 +190,37 @@ void main() {
|
|||
verify(() => mockNativeApi.hashAssets([asset2.id], allowNetworkAccess: false)).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('HashService hashAssets (trash sync mode)', () {
|
||||
test('hashes trashed assets and writes to trashed repo', () async {
|
||||
final album = LocalAlbumStub.recent;
|
||||
final trashed1 = TrashedAssetStub.trashed1.copyWith(id: 't1');
|
||||
|
||||
when(() => mockAlbumRepo.getBackupAlbums()).thenAnswer((_) async => [album]);
|
||||
when(() => mockTrashedAssetRepo.getAssetsToHash([album.id])).thenAnswer((_) async => [trashed1]);
|
||||
|
||||
when(
|
||||
() => mockNativeApi.hashAssets([trashed1.id], allowNetworkAccess: false),
|
||||
).thenAnswer((_) async => [HashResult(assetId: trashed1.id, hash: 't1-hash')]);
|
||||
|
||||
await sut.hashAssets();
|
||||
|
||||
verify(() => mockNativeApi.hashAssets([trashed1.id], allowNetworkAccess: false)).called(1);
|
||||
verify(() => mockTrashedAssetRepo.updateHashes({'t1': 't1-hash'})).called(1);
|
||||
verifyNever(() => mockAssetRepo.updateHashes(any()));
|
||||
});
|
||||
|
||||
test('skips when trashed list is empty', () async {
|
||||
final album = LocalAlbumStub.recent;
|
||||
|
||||
when(() => mockAlbumRepo.getBackupAlbums()).thenAnswer((_) async => [album]);
|
||||
when(() => mockTrashedAssetRepo.getAssetsToHash([album.id])).thenAnswer((_) async => []);
|
||||
|
||||
await sut.hashAssets();
|
||||
|
||||
verifyNever(() => mockNativeApi.hashAssets(any(), allowNetworkAccess: any(named: 'allowNetworkAccess')));
|
||||
verifyNever(() => mockTrashedAssetRepo.updateHashes(any()));
|
||||
verifyNever(() => mockAssetRepo.updateHashes(any()));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
14
mobile/test/fixtures/asset.stub.dart
vendored
14
mobile/test/fixtures/asset.stub.dart
vendored
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/trashed_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart' as old;
|
||||
|
||||
|
|
@ -74,3 +75,16 @@ abstract final class LocalAssetStub {
|
|||
updatedAt: DateTime(20021),
|
||||
);
|
||||
}
|
||||
|
||||
abstract final class TrashedAssetStub {
|
||||
const TrashedAssetStub._();
|
||||
|
||||
static final trashed1 = TrashedAsset(
|
||||
id: "t1",
|
||||
name: "trashed1.jpg",
|
||||
type: AssetType.image,
|
||||
createdAt: DateTime(2025, 1, 1),
|
||||
updatedAt: DateTime(2025, 1, 2),
|
||||
albumId: "album1",
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:immich_mobile/infrastructure/repositories/storage.repository.dar
|
|||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||
|
|
@ -30,6 +31,8 @@ class MockRemoteAlbumRepository extends Mock implements DriftRemoteAlbumReposito
|
|||
|
||||
class MockLocalAssetRepository extends Mock implements DriftLocalAssetRepository {}
|
||||
|
||||
class MockTrashedLocalAssetRepository extends Mock implements DriftTrashedLocalAssetRepository {}
|
||||
|
||||
class MockStorageRepository extends Mock implements StorageRepository {}
|
||||
|
||||
// API Repos
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue