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 height: Long? = null,
|
||||||
val durationInSeconds: Long,
|
val durationInSeconds: Long,
|
||||||
val orientation: Long,
|
val orientation: Long,
|
||||||
val isFavorite: Boolean,
|
val isFavorite: Boolean
|
||||||
val isTrashed: Boolean? = null,
|
|
||||||
val volume: String? = null
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
companion object {
|
companion object {
|
||||||
|
|
@ -106,9 +104,7 @@ data class PlatformAsset (
|
||||||
val durationInSeconds = pigeonVar_list[7] as Long
|
val durationInSeconds = pigeonVar_list[7] as Long
|
||||||
val orientation = pigeonVar_list[8] as Long
|
val orientation = pigeonVar_list[8] as Long
|
||||||
val isFavorite = pigeonVar_list[9] as Boolean
|
val isFavorite = pigeonVar_list[9] as Boolean
|
||||||
val isTrashed = pigeonVar_list[10] as Boolean?
|
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite)
|
||||||
val volume = pigeonVar_list[11] as String?
|
|
||||||
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite, isTrashed, volume)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun toList(): List<Any?> {
|
fun toList(): List<Any?> {
|
||||||
|
|
@ -123,8 +119,6 @@ data class PlatformAsset (
|
||||||
durationInSeconds,
|
durationInSeconds,
|
||||||
orientation,
|
orientation,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
isTrashed,
|
|
||||||
volume,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
|
|
@ -249,40 +243,6 @@ data class HashResult (
|
||||||
|
|
||||||
override fun hashCode(): Int = toList().hashCode()
|
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() {
|
private open class MessagesPigeonCodec : StandardMessageCodec() {
|
||||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||||
return when (type) {
|
return when (type) {
|
||||||
|
|
@ -306,11 +266,6 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
|
||||||
HashResult.fromList(it)
|
HashResult.fromList(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
133.toByte() -> {
|
|
||||||
return (readValue(buffer) as? List<Any?>)?.let {
|
|
||||||
TrashedAssetParams.fromList(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> super.readValueOfType(type, buffer)
|
else -> super.readValueOfType(type, buffer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -332,10 +287,6 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
|
||||||
stream.write(132)
|
stream.write(132)
|
||||||
writeValue(stream, value.toList())
|
writeValue(stream, value.toList())
|
||||||
}
|
}
|
||||||
is TrashedAssetParams -> {
|
|
||||||
stream.write(133)
|
|
||||||
writeValue(stream, value.toList())
|
|
||||||
}
|
|
||||||
else -> super.writeValue(stream, value)
|
else -> super.writeValue(stream, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -355,7 +306,6 @@ interface NativeSyncApi {
|
||||||
fun hashAssets(assetIds: List<String>, allowNetworkAccess: Boolean, callback: (Result<List<HashResult>>) -> Unit)
|
fun hashAssets(assetIds: List<String>, allowNetworkAccess: Boolean, callback: (Result<List<HashResult>>) -> Unit)
|
||||||
fun cancelHashing()
|
fun cancelHashing()
|
||||||
fun getTrashedAssetsForAlbum(albumId: String): List<PlatformAsset>
|
fun getTrashedAssetsForAlbum(albumId: String): List<PlatformAsset>
|
||||||
fun hashTrashedAssets(trashedAssets: List<TrashedAssetParams>, callback: (Result<List<HashResult>>) -> Unit)
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** The codec used by NativeSyncApi. */
|
/** The codec used by NativeSyncApi. */
|
||||||
|
|
@ -553,26 +503,6 @@ interface NativeSyncApi {
|
||||||
channel.setMessageHandler(null)
|
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> {
|
): List<PlatformAsset> {
|
||||||
throw IllegalStateException("Method not supported on this Android version.")
|
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
|
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
|
// only available on Android 11 and above
|
||||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
add(MediaStore.MediaColumns.IS_FAVORITE)
|
add(MediaStore.MediaColumns.IS_FAVORITE)
|
||||||
add(MediaStore.MediaColumns.IS_TRASHED)
|
|
||||||
add(MediaStore.MediaColumns.VOLUME_NAME)
|
|
||||||
}
|
}
|
||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
|
|
||||||
|
|
@ -111,8 +109,6 @@ open class NativeSyncApiImplBase(context: Context) {
|
||||||
val orientationColumn =
|
val orientationColumn =
|
||||||
c.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION)
|
c.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION)
|
||||||
val favoriteColumn = c.getColumnIndex(MediaStore.MediaColumns.IS_FAVORITE)
|
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()) {
|
while (c.moveToNext()) {
|
||||||
val id = c.getLong(idColumn).toString()
|
val id = c.getLong(idColumn).toString()
|
||||||
|
|
@ -142,8 +138,6 @@ open class NativeSyncApiImplBase(context: Context) {
|
||||||
val bucketId = c.getString(bucketIdColumn)
|
val bucketId = c.getString(bucketIdColumn)
|
||||||
val orientation = c.getInt(orientationColumn)
|
val orientation = c.getInt(orientationColumn)
|
||||||
val isFavorite = if (favoriteColumn == -1) false else c.getInt(favoriteColumn) != 0
|
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(
|
val asset = PlatformAsset(
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
|
|
@ -155,8 +149,6 @@ open class NativeSyncApiImplBase(context: Context) {
|
||||||
duration,
|
duration,
|
||||||
orientation.toLong(),
|
orientation.toLong(),
|
||||||
isFavorite,
|
isFavorite,
|
||||||
isTrashed,
|
|
||||||
volume,
|
|
||||||
)
|
)
|
||||||
yield(AssetResult.ValidAsset(asset, bucketId))
|
yield(AssetResult.ValidAsset(asset, bucketId))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -246,46 +246,46 @@ EXTERNAL SOURCES:
|
||||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad
|
background_downloader: a05c77d32a0d70615b9c04577aa203535fc924ff
|
||||||
bonsoir_darwin: 29c7ccf356646118844721f36e1de4b61f6cbd0e
|
bonsoir_darwin: e3b8526c42ca46a885142df84229131dfabea842
|
||||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
|
||||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
device_info_plus: bf2e3232933866d73fe290f2942f2156cdd10342
|
||||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
|
||||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
|
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
||||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
|
||||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||||
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab
|
||||||
flutter_web_auth_2: 5c8d9dcd7848b5a9efb086d24e7a9adcae979c80
|
flutter_web_auth_2: 06d500582775790a0d4c323222fcb6d7990f9603
|
||||||
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
|
fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
|
||||||
geolocator_apple: 1560c3c875af2a412242c7a923e15d0d401966ff
|
geolocator_apple: 9bcea1918ff7f0062d98345d238ae12718acfbc1
|
||||||
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57
|
||||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
|
||||||
isar_flutter_libs: bc909e72c3d756c2759f14c8776c13b5b0556e26
|
isar_flutter_libs: fdf730ca925d05687f36d7f1d355e482529ed097
|
||||||
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
|
local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
|
||||||
MapLibre: 69e572367f4ef6287e18246cfafc39c80cdcabcd
|
MapLibre: 69e572367f4ef6287e18246cfafc39c80cdcabcd
|
||||||
maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f
|
maplibre_gl: 753f55d763a81cbdba087d02af02d12206e6f94e
|
||||||
native_video_player: b65c58951ede2f93d103a25366bdebca95081265
|
native_video_player: d12af78a1a4a8cf09775a5177d5b392def6fd23c
|
||||||
network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc
|
network_info_plus: 6613d9d7cdeb0e6f366ed4dbe4b3c51c52d567a9
|
||||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||||
photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413
|
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
|
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
|
||||||
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
|
share_handler_ios: 6dd3a4ac5ca0d955274aec712ba0ecdcaf583e7c
|
||||||
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
|
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
|
||||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||||
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
|
||||||
sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241
|
sqlite3_flutter_libs: cc304edcb8e1d8c595d1b08c7aeb46a47691d9db
|
||||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||||
wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49
|
wakelock_plus: 373cfe59b235a6dd5837d0fb88791d2f13a90d56
|
||||||
|
|
||||||
PODFILE CHECKSUM: 7ce312f2beab01395db96f6969d90a447279cf45
|
PODFILE CHECKSUM: 7ce312f2beab01395db96f6969d90a447279cf45
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -140,8 +140,6 @@ struct PlatformAsset: Hashable {
|
||||||
var durationInSeconds: Int64
|
var durationInSeconds: Int64
|
||||||
var orientation: Int64
|
var orientation: Int64
|
||||||
var isFavorite: Bool
|
var isFavorite: Bool
|
||||||
var isTrashed: Bool? = nil
|
|
||||||
var volume: String? = nil
|
|
||||||
|
|
||||||
|
|
||||||
// swift-format-ignore: AlwaysUseLowerCamelCase
|
// swift-format-ignore: AlwaysUseLowerCamelCase
|
||||||
|
|
@ -156,8 +154,6 @@ struct PlatformAsset: Hashable {
|
||||||
let durationInSeconds = pigeonVar_list[7] as! Int64
|
let durationInSeconds = pigeonVar_list[7] as! Int64
|
||||||
let orientation = pigeonVar_list[8] as! Int64
|
let orientation = pigeonVar_list[8] as! Int64
|
||||||
let isFavorite = pigeonVar_list[9] as! Bool
|
let isFavorite = pigeonVar_list[9] as! Bool
|
||||||
let isTrashed: Bool? = nilOrValue(pigeonVar_list[10])
|
|
||||||
let volume: String? = nilOrValue(pigeonVar_list[11])
|
|
||||||
|
|
||||||
return PlatformAsset(
|
return PlatformAsset(
|
||||||
id: id,
|
id: id,
|
||||||
|
|
@ -169,9 +165,7 @@ struct PlatformAsset: Hashable {
|
||||||
height: height,
|
height: height,
|
||||||
durationInSeconds: durationInSeconds,
|
durationInSeconds: durationInSeconds,
|
||||||
orientation: orientation,
|
orientation: orientation,
|
||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite
|
||||||
isTrashed: isTrashed,
|
|
||||||
volume: volume
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
func toList() -> [Any?] {
|
func toList() -> [Any?] {
|
||||||
|
|
@ -186,8 +180,6 @@ struct PlatformAsset: Hashable {
|
||||||
durationInSeconds,
|
durationInSeconds,
|
||||||
orientation,
|
orientation,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
isTrashed,
|
|
||||||
volume,
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
|
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 {
|
private class MessagesPigeonCodecReader: FlutterStandardReader {
|
||||||
override func readValue(ofType type: UInt8) -> Any? {
|
override func readValue(ofType type: UInt8) -> Any? {
|
||||||
switch type {
|
switch type {
|
||||||
|
|
@ -352,8 +311,6 @@ private class MessagesPigeonCodecReader: FlutterStandardReader {
|
||||||
return SyncDelta.fromList(self.readValue() as! [Any?])
|
return SyncDelta.fromList(self.readValue() as! [Any?])
|
||||||
case 132:
|
case 132:
|
||||||
return HashResult.fromList(self.readValue() as! [Any?])
|
return HashResult.fromList(self.readValue() as! [Any?])
|
||||||
case 133:
|
|
||||||
return TrashedAssetParams.fromList(self.readValue() as! [Any?])
|
|
||||||
default:
|
default:
|
||||||
return super.readValue(ofType: type)
|
return super.readValue(ofType: type)
|
||||||
}
|
}
|
||||||
|
|
@ -374,9 +331,6 @@ private class MessagesPigeonCodecWriter: FlutterStandardWriter {
|
||||||
} else if let value = value as? HashResult {
|
} else if let value = value as? HashResult {
|
||||||
super.writeByte(132)
|
super.writeByte(132)
|
||||||
super.writeValue(value.toList())
|
super.writeValue(value.toList())
|
||||||
} else if let value = value as? TrashedAssetParams {
|
|
||||||
super.writeByte(133)
|
|
||||||
super.writeValue(value.toList())
|
|
||||||
} else {
|
} else {
|
||||||
super.writeValue(value)
|
super.writeValue(value)
|
||||||
}
|
}
|
||||||
|
|
@ -411,7 +365,6 @@ protocol NativeSyncApi {
|
||||||
func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void)
|
func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void)
|
||||||
func cancelHashing() throws
|
func cancelHashing() throws
|
||||||
func getTrashedAssetsForAlbum(albumId: String) throws -> [PlatformAsset]
|
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`.
|
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||||
|
|
@ -599,24 +552,5 @@ class NativeSyncApiSetup {
|
||||||
} else {
|
} else {
|
||||||
getTrashedAssetsForAlbumChannel.setMessageHandler(nil)
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,15 @@ import CryptoKit
|
||||||
|
|
||||||
struct AssetWrapper: Hashable, Equatable {
|
struct AssetWrapper: Hashable, Equatable {
|
||||||
let asset: PlatformAsset
|
let asset: PlatformAsset
|
||||||
|
|
||||||
init(with asset: PlatformAsset) {
|
init(with asset: PlatformAsset) {
|
||||||
self.asset = asset
|
self.asset = asset
|
||||||
}
|
}
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(self.asset.id)
|
hasher.combine(self.asset.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func == (lhs: AssetWrapper, rhs: AssetWrapper) -> Bool {
|
static func == (lhs: AssetWrapper, rhs: AssetWrapper) -> Bool {
|
||||||
return lhs.asset.id == rhs.asset.id
|
return lhs.asset.id == rhs.asset.id
|
||||||
}
|
}
|
||||||
|
|
@ -22,16 +22,16 @@ class NativeSyncApiImpl: NativeSyncApi {
|
||||||
private let changeTokenKey = "immich:changeToken"
|
private let changeTokenKey = "immich:changeToken"
|
||||||
private let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum]
|
private let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum]
|
||||||
private let recoveredAlbumSubType = 1000000219
|
private let recoveredAlbumSubType = 1000000219
|
||||||
|
|
||||||
private var hashTask: Task<Void, Error>?
|
private var hashTask: Task<Void, Error>?
|
||||||
private static let hashCancelledCode = "HASH_CANCELLED"
|
private static let hashCancelledCode = "HASH_CANCELLED"
|
||||||
private static let hashCancelled = Result<[HashResult], Error>.failure(PigeonError(code: hashCancelledCode, message: "Hashing cancelled", details: nil))
|
private static let hashCancelled = Result<[HashResult], Error>.failure(PigeonError(code: hashCancelledCode, message: "Hashing cancelled", details: nil))
|
||||||
|
|
||||||
|
|
||||||
init(with defaults: UserDefaults = .standard) {
|
init(with defaults: UserDefaults = .standard) {
|
||||||
self.defaults = defaults
|
self.defaults = defaults
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 16, *)
|
@available(iOS 16, *)
|
||||||
private func getChangeToken() -> PHPersistentChangeToken? {
|
private func getChangeToken() -> PHPersistentChangeToken? {
|
||||||
guard let data = defaults.data(forKey: changeTokenKey) else {
|
guard let data = defaults.data(forKey: changeTokenKey) else {
|
||||||
|
|
@ -39,7 +39,7 @@ class NativeSyncApiImpl: NativeSyncApi {
|
||||||
}
|
}
|
||||||
return try? NSKeyedUnarchiver.unarchivedObject(ofClass: PHPersistentChangeToken.self, from: data)
|
return try? NSKeyedUnarchiver.unarchivedObject(ofClass: PHPersistentChangeToken.self, from: data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 16, *)
|
@available(iOS 16, *)
|
||||||
private func saveChangeToken(token: PHPersistentChangeToken) -> Void {
|
private func saveChangeToken(token: PHPersistentChangeToken) -> Void {
|
||||||
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true) else {
|
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true) else {
|
||||||
|
|
@ -47,18 +47,18 @@ class NativeSyncApiImpl: NativeSyncApi {
|
||||||
}
|
}
|
||||||
defaults.set(data, forKey: changeTokenKey)
|
defaults.set(data, forKey: changeTokenKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearSyncCheckpoint() -> Void {
|
func clearSyncCheckpoint() -> Void {
|
||||||
defaults.removeObject(forKey: changeTokenKey)
|
defaults.removeObject(forKey: changeTokenKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkpointSync() {
|
func checkpointSync() {
|
||||||
guard #available(iOS 16, *) else {
|
guard #available(iOS 16, *) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
saveChangeToken(token: PHPhotoLibrary.shared().currentChangeToken)
|
saveChangeToken(token: PHPhotoLibrary.shared().currentChangeToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
func shouldFullSync() -> Bool {
|
func shouldFullSync() -> Bool {
|
||||||
guard #available(iOS 16, *),
|
guard #available(iOS 16, *),
|
||||||
PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized,
|
PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized,
|
||||||
|
|
@ -66,34 +66,34 @@ class NativeSyncApiImpl: NativeSyncApi {
|
||||||
// When we do not have access to photo library, older iOS version or No token available, fallback to full sync
|
// When we do not have access to photo library, older iOS version or No token available, fallback to full sync
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let _ = try? PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken) else {
|
guard let _ = try? PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken) else {
|
||||||
// Cannot fetch persistent changes
|
// Cannot fetch persistent changes
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAlbums() throws -> [PlatformAlbum] {
|
func getAlbums() throws -> [PlatformAlbum] {
|
||||||
var albums: [PlatformAlbum] = []
|
var albums: [PlatformAlbum] = []
|
||||||
|
|
||||||
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)
|
||||||
for i in 0..<collections.count {
|
for i in 0..<collections.count {
|
||||||
let album = collections.object(at: i)
|
let album = collections.object(at: i)
|
||||||
|
|
||||||
// Ignore recovered album
|
// Ignore recovered album
|
||||||
if(album.assetCollectionSubtype.rawValue == self.recoveredAlbumSubType) {
|
if(album.assetCollectionSubtype.rawValue == self.recoveredAlbumSubType) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let options = PHFetchOptions()
|
let options = PHFetchOptions()
|
||||||
options.sortDescriptors = [NSSortDescriptor(key: "modificationDate", ascending: false)]
|
options.sortDescriptors = [NSSortDescriptor(key: "modificationDate", ascending: false)]
|
||||||
options.includeHiddenAssets = false
|
options.includeHiddenAssets = false
|
||||||
let assets = PHAsset.fetchAssets(in: album, options: options)
|
let assets = PHAsset.fetchAssets(in: album, options: options)
|
||||||
let isCloud = album.assetCollectionSubtype == .albumCloudShared || album.assetCollectionSubtype == .albumMyPhotoStream
|
let isCloud = album.assetCollectionSubtype == .albumCloudShared || album.assetCollectionSubtype == .albumMyPhotoStream
|
||||||
|
|
||||||
var domainAlbum = PlatformAlbum(
|
var domainAlbum = PlatformAlbum(
|
||||||
id: album.localIdentifier,
|
id: album.localIdentifier,
|
||||||
name: album.localizedTitle!,
|
name: album.localizedTitle!,
|
||||||
|
|
@ -101,57 +101,57 @@ class NativeSyncApiImpl: NativeSyncApi {
|
||||||
isCloud: isCloud,
|
isCloud: isCloud,
|
||||||
assetCount: Int64(assets.count)
|
assetCount: Int64(assets.count)
|
||||||
)
|
)
|
||||||
|
|
||||||
if let firstAsset = assets.firstObject {
|
if let firstAsset = assets.firstObject {
|
||||||
domainAlbum.updatedAt = firstAsset.modificationDate.map { Int64($0.timeIntervalSince1970) }
|
domainAlbum.updatedAt = firstAsset.modificationDate.map { Int64($0.timeIntervalSince1970) }
|
||||||
}
|
}
|
||||||
|
|
||||||
albums.append(domainAlbum)
|
albums.append(domainAlbum)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return albums.sorted { $0.id < $1.id }
|
return albums.sorted { $0.id < $1.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMediaChanges() throws -> SyncDelta {
|
func getMediaChanges(isTrashed: Bool) throws -> SyncDelta {
|
||||||
guard #available(iOS 16, *) else {
|
guard #available(iOS 16, *) else {
|
||||||
throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature requires iOS 16 or later.", details: nil)
|
throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature requires iOS 16 or later.", details: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else {
|
guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else {
|
||||||
throw PigeonError(code: "NO_AUTH", message: "No photo library access", details: nil)
|
throw PigeonError(code: "NO_AUTH", message: "No photo library access", details: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let storedToken = getChangeToken() else {
|
guard let storedToken = getChangeToken() else {
|
||||||
// No token exists, definitely need a full sync
|
// No token exists, definitely need a full sync
|
||||||
print("MediaManager::getMediaChanges: No token found")
|
print("MediaManager::getMediaChanges: No token found")
|
||||||
throw PigeonError(code: "NO_TOKEN", message: "No stored change token", details: nil)
|
throw PigeonError(code: "NO_TOKEN", message: "No stored change token", details: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentToken = PHPhotoLibrary.shared().currentChangeToken
|
let currentToken = PHPhotoLibrary.shared().currentChangeToken
|
||||||
if storedToken == currentToken {
|
if storedToken == currentToken {
|
||||||
return SyncDelta(hasChanges: false, updates: [], deletes: [], assetAlbums: [:])
|
return SyncDelta(hasChanges: false, updates: [], deletes: [], assetAlbums: [:])
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let changes = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken)
|
let changes = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken)
|
||||||
|
|
||||||
var updatedAssets: Set<AssetWrapper> = []
|
var updatedAssets: Set<AssetWrapper> = []
|
||||||
var deletedAssets: Set<String> = []
|
var deletedAssets: Set<String> = []
|
||||||
|
|
||||||
for change in changes {
|
for change in changes {
|
||||||
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)
|
deletedAssets.formUnion(details.deletedLocalIdentifiers)
|
||||||
|
|
||||||
if (updated.isEmpty) { continue }
|
if (updated.isEmpty) { continue }
|
||||||
|
|
||||||
let options = PHFetchOptions()
|
let options = PHFetchOptions()
|
||||||
options.includeHiddenAssets = false
|
options.includeHiddenAssets = false
|
||||||
let result = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: options)
|
let result = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: options)
|
||||||
for i in 0..<result.count {
|
for i in 0..<result.count {
|
||||||
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(
|
let predicate = PlatformAsset(
|
||||||
id: asset.localIdentifier,
|
id: asset.localIdentifier,
|
||||||
|
|
@ -164,25 +164,25 @@ class NativeSyncApiImpl: NativeSyncApi {
|
||||||
if (updatedAssets.contains(AssetWrapper(with: predicate))) {
|
if (updatedAssets.contains(AssetWrapper(with: predicate))) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
let domainAsset = AssetWrapper(with: asset.toPlatformAsset())
|
let domainAsset = AssetWrapper(with: asset.toPlatformAsset())
|
||||||
updatedAssets.insert(domainAsset)
|
updatedAssets.insert(domainAsset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let updates = Array(updatedAssets.map { $0.asset })
|
let updates = Array(updatedAssets.map { $0.asset })
|
||||||
return SyncDelta(hasChanges: true, updates: updates, deletes: Array(deletedAssets), assetAlbums: buildAssetAlbumsMap(assets: updates))
|
return SyncDelta(hasChanges: true, updates: updates, deletes: Array(deletedAssets), assetAlbums: buildAssetAlbumsMap(assets: updates))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private func buildAssetAlbumsMap(assets: Array<PlatformAsset>) -> [String: [String]] {
|
private func buildAssetAlbumsMap(assets: Array<PlatformAsset>) -> [String: [String]] {
|
||||||
guard !assets.isEmpty else {
|
guard !assets.isEmpty else {
|
||||||
return [:]
|
return [:]
|
||||||
}
|
}
|
||||||
|
|
||||||
var albumAssets: [String: [String]] = [:]
|
var albumAssets: [String: [String]] = [:]
|
||||||
|
|
||||||
for type in albumTypes {
|
for type in albumTypes {
|
||||||
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
|
||||||
|
|
@ -197,13 +197,13 @@ class NativeSyncApiImpl: NativeSyncApi {
|
||||||
}
|
}
|
||||||
return albumAssets
|
return albumAssets
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAssetIdsForAlbum(albumId: String) throws -> [String] {
|
func getAssetIdsForAlbum(albumId: String) throws -> [String] {
|
||||||
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
|
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
|
||||||
guard let album = collections.firstObject else {
|
guard let album = collections.firstObject else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
var ids: [String] = []
|
var ids: [String] = []
|
||||||
let options = PHFetchOptions()
|
let options = PHFetchOptions()
|
||||||
options.includeHiddenAssets = false
|
options.includeHiddenAssets = false
|
||||||
|
|
@ -213,13 +213,13 @@ class NativeSyncApiImpl: NativeSyncApi {
|
||||||
}
|
}
|
||||||
return ids
|
return ids
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 {
|
func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 {
|
||||||
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
|
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
|
||||||
guard let album = collections.firstObject else {
|
guard let album = collections.firstObject else {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
let date = NSDate(timeIntervalSince1970: TimeInterval(timestamp))
|
let date = NSDate(timeIntervalSince1970: TimeInterval(timestamp))
|
||||||
let options = PHFetchOptions()
|
let options = PHFetchOptions()
|
||||||
options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date)
|
options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date)
|
||||||
|
|
@ -227,32 +227,32 @@ class NativeSyncApiImpl: NativeSyncApi {
|
||||||
let assets = PHAsset.fetchAssets(in: album, options: options)
|
let assets = PHAsset.fetchAssets(in: album, options: options)
|
||||||
return Int64(assets.count)
|
return Int64(assets.count)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] {
|
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] {
|
||||||
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
|
let collections = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [albumId], options: nil)
|
||||||
guard let album = collections.firstObject else {
|
guard let album = collections.firstObject else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
let options = PHFetchOptions()
|
let options = PHFetchOptions()
|
||||||
options.includeHiddenAssets = false
|
options.includeHiddenAssets = false
|
||||||
if(updatedTimeCond != nil) {
|
if(updatedTimeCond != nil) {
|
||||||
let date = NSDate(timeIntervalSince1970: TimeInterval(updatedTimeCond!))
|
let date = NSDate(timeIntervalSince1970: TimeInterval(updatedTimeCond!))
|
||||||
options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date)
|
options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date)
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = PHAsset.fetchAssets(in: album, options: options)
|
let result = PHAsset.fetchAssets(in: album, options: options)
|
||||||
if(result.count == 0) {
|
if(result.count == 0) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
var assets: [PlatformAsset] = []
|
var assets: [PlatformAsset] = []
|
||||||
result.enumerateObjects { (asset, _, _) in
|
result.enumerateObjects { (asset, _, _) in
|
||||||
assets.append(asset.toPlatformAsset())
|
assets.append(asset.toPlatformAsset())
|
||||||
}
|
}
|
||||||
return assets
|
return assets
|
||||||
}
|
}
|
||||||
|
|
||||||
func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void) {
|
func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void) {
|
||||||
if let prevTask = hashTask {
|
if let prevTask = hashTask {
|
||||||
prevTask.cancel()
|
prevTask.cancel()
|
||||||
|
|
@ -270,11 +270,11 @@ class NativeSyncApiImpl: NativeSyncApi {
|
||||||
missingAssetIds.remove(asset.localIdentifier)
|
missingAssetIds.remove(asset.localIdentifier)
|
||||||
assets.append(asset)
|
assets.append(asset)
|
||||||
}
|
}
|
||||||
|
|
||||||
if Task.isCancelled {
|
if Task.isCancelled {
|
||||||
return completion(Self.hashCancelled)
|
return completion(Self.hashCancelled)
|
||||||
}
|
}
|
||||||
|
|
||||||
await withTaskGroup(of: HashResult?.self) { taskGroup in
|
await withTaskGroup(of: HashResult?.self) { taskGroup in
|
||||||
var results = [HashResult]()
|
var results = [HashResult]()
|
||||||
results.reserveCapacity(assets.count)
|
results.reserveCapacity(assets.count)
|
||||||
|
|
@ -287,28 +287,28 @@ class NativeSyncApiImpl: NativeSyncApi {
|
||||||
return await self.hashAsset(asset, allowNetworkAccess: allowNetworkAccess)
|
return await self.hashAsset(asset, allowNetworkAccess: allowNetworkAccess)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for await result in taskGroup {
|
for await result in taskGroup {
|
||||||
guard let result = result else {
|
guard let result = result else {
|
||||||
return completion(Self.hashCancelled)
|
return completion(Self.hashCancelled)
|
||||||
}
|
}
|
||||||
results.append(result)
|
results.append(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
for missing in missingAssetIds {
|
for missing in missingAssetIds {
|
||||||
results.append(HashResult(assetId: missing, error: "Asset not found in library", hash: nil))
|
results.append(HashResult(assetId: missing, error: "Asset not found in library", hash: nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
completion(.success(results))
|
completion(.success(results))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func cancelHashing() {
|
func cancelHashing() {
|
||||||
hashTask?.cancel()
|
hashTask?.cancel()
|
||||||
hashTask = nil
|
hashTask = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func hashAsset(_ asset: PHAsset, allowNetworkAccess: Bool) async -> HashResult? {
|
private func hashAsset(_ asset: PHAsset, allowNetworkAccess: Bool) async -> HashResult? {
|
||||||
class RequestRef {
|
class RequestRef {
|
||||||
var id: PHAssetResourceDataRequestID?
|
var id: PHAssetResourceDataRequestID?
|
||||||
|
|
@ -318,21 +318,21 @@ class NativeSyncApiImpl: NativeSyncApi {
|
||||||
if Task.isCancelled {
|
if Task.isCancelled {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let resource = asset.getResource() else {
|
guard let resource = asset.getResource() else {
|
||||||
return HashResult(assetId: asset.localIdentifier, error: "Cannot get asset resource", hash: nil)
|
return HashResult(assetId: asset.localIdentifier, error: "Cannot get asset resource", hash: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if Task.isCancelled {
|
if Task.isCancelled {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let options = PHAssetResourceRequestOptions()
|
let options = PHAssetResourceRequestOptions()
|
||||||
options.isNetworkAccessAllowed = allowNetworkAccess
|
options.isNetworkAccessAllowed = allowNetworkAccess
|
||||||
|
|
||||||
return await withCheckedContinuation { continuation in
|
return await withCheckedContinuation { continuation in
|
||||||
var hasher = Insecure.SHA1()
|
var hasher = Insecure.SHA1()
|
||||||
|
|
||||||
requestRef.id = PHAssetResourceManager.default().requestData(
|
requestRef.id = PHAssetResourceManager.default().requestData(
|
||||||
for: resource,
|
for: resource,
|
||||||
options: options,
|
options: options,
|
||||||
|
|
@ -363,4 +363,9 @@ class NativeSyncApiImpl: NativeSyncApi {
|
||||||
PHAssetResourceManager.default().cancelDataRequest(requestId)
|
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';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
|
||||||
class TrashedAsset extends LocalAsset {
|
class TrashedAsset extends LocalAsset {
|
||||||
final String? volume;
|
|
||||||
final String albumId;
|
final String albumId;
|
||||||
|
|
||||||
const TrashedAsset({
|
const TrashedAsset({
|
||||||
required this.albumId,
|
required this.albumId,
|
||||||
this.volume,
|
|
||||||
required super.id,
|
required super.id,
|
||||||
super.remoteId,
|
super.remoteId,
|
||||||
required super.name,
|
required super.name,
|
||||||
|
|
@ -38,7 +36,6 @@ class TrashedAsset extends LocalAsset {
|
||||||
String? livePhotoVideoId,
|
String? livePhotoVideoId,
|
||||||
int? orientation,
|
int? orientation,
|
||||||
String? albumId,
|
String? albumId,
|
||||||
String? volume,
|
|
||||||
}) {
|
}) {
|
||||||
return TrashedAsset(
|
return TrashedAsset(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
|
|
@ -55,7 +52,6 @@ class TrashedAsset extends LocalAsset {
|
||||||
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||||
orientation: orientation ?? this.orientation,
|
orientation: orientation ?? this.orientation,
|
||||||
albumId: albumId ?? this.albumId,
|
albumId: albumId ?? this.albumId,
|
||||||
volume: volume ?? this.volume,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,7 +75,6 @@ class TrashedAsset extends LocalAsset {
|
||||||
livePhotoVideoId == other.livePhotoVideoId &&
|
livePhotoVideoId == other.livePhotoVideoId &&
|
||||||
orientation == other.orientation &&
|
orientation == other.orientation &&
|
||||||
// TrashedAsset extras
|
// TrashedAsset extras
|
||||||
volume == other.volume &&
|
|
||||||
albumId == other.albumId;
|
albumId == other.albumId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -97,7 +92,6 @@ class TrashedAsset extends LocalAsset {
|
||||||
isFavorite,
|
isFavorite,
|
||||||
livePhotoVideoId,
|
livePhotoVideoId,
|
||||||
orientation,
|
orientation,
|
||||||
volume,
|
|
||||||
albumId,
|
albumId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -118,7 +112,6 @@ class TrashedAsset extends LocalAsset {
|
||||||
'livePhotoVideoId: $livePhotoVideoId, '
|
'livePhotoVideoId: $livePhotoVideoId, '
|
||||||
'orientation: $orientation, '
|
'orientation: $orientation, '
|
||||||
'albumId: $albumId, '
|
'albumId: $albumId, '
|
||||||
'volume: $volume'
|
|
||||||
')';
|
')';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ import 'package:flutter/services.dart';
|
||||||
import 'package:immich_mobile/constants/constants.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/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/trashed_asset.model.dart';
|
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||||
import 'package:immich_mobile/domain/services/trash_sync.service.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.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/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:immich_mobile/platform/native_sync_api.g.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
|
@ -15,24 +15,24 @@ class HashService {
|
||||||
final int _batchSize;
|
final int _batchSize;
|
||||||
final DriftLocalAlbumRepository _localAlbumRepository;
|
final DriftLocalAlbumRepository _localAlbumRepository;
|
||||||
final DriftLocalAssetRepository _localAssetRepository;
|
final DriftLocalAssetRepository _localAssetRepository;
|
||||||
|
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
||||||
final NativeSyncApi _nativeSyncApi;
|
final NativeSyncApi _nativeSyncApi;
|
||||||
final TrashSyncService _trashSyncService;
|
|
||||||
final bool Function()? _cancelChecker;
|
final bool Function()? _cancelChecker;
|
||||||
final _log = Logger('HashService');
|
final _log = Logger('HashService');
|
||||||
|
|
||||||
HashService({
|
HashService({
|
||||||
required DriftLocalAlbumRepository localAlbumRepository,
|
required DriftLocalAlbumRepository localAlbumRepository,
|
||||||
required DriftLocalAssetRepository localAssetRepository,
|
required DriftLocalAssetRepository localAssetRepository,
|
||||||
|
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
|
||||||
required NativeSyncApi nativeSyncApi,
|
required NativeSyncApi nativeSyncApi,
|
||||||
required TrashSyncService trashSyncService,
|
|
||||||
bool Function()? cancelChecker,
|
bool Function()? cancelChecker,
|
||||||
int? batchSize,
|
int? batchSize,
|
||||||
}) : _localAlbumRepository = localAlbumRepository,
|
}) : _localAlbumRepository = localAlbumRepository,
|
||||||
_localAssetRepository = localAssetRepository,
|
_localAssetRepository = localAssetRepository,
|
||||||
|
_trashedLocalAssetRepository = trashedLocalAssetRepository,
|
||||||
_cancelChecker = cancelChecker,
|
_cancelChecker = cancelChecker,
|
||||||
_nativeSyncApi = nativeSyncApi,
|
_nativeSyncApi = nativeSyncApi,
|
||||||
_batchSize = batchSize ?? kBatchHashFileLimit,
|
_batchSize = batchSize ?? kBatchHashFileLimit;
|
||||||
_trashSyncService = trashSyncService;
|
|
||||||
|
|
||||||
bool get isCancelled => _cancelChecker?.call() ?? false;
|
bool get isCancelled => _cancelChecker?.call() ?? false;
|
||||||
|
|
||||||
|
|
@ -54,6 +54,14 @@ class HashService {
|
||||||
await _hashAssets(album, assetsToHash);
|
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) {
|
} on PlatformException catch (e) {
|
||||||
if (e.code == _kHashCancelledCode) {
|
if (e.code == _kHashCancelledCode) {
|
||||||
_log.warning("Hashing cancelled by platform");
|
_log.warning("Hashing cancelled by platform");
|
||||||
|
|
@ -63,23 +71,6 @@ class HashService {
|
||||||
_log.severe("Error during hashing", e, s);
|
_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();
|
stopwatch.stop();
|
||||||
_log.info("Hashing took - ${stopwatch.elapsedMilliseconds}ms");
|
_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
|
/// 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
|
/// 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.
|
/// [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>{};
|
final toHash = <String, LocalAsset>{};
|
||||||
|
|
||||||
for (final asset in assetsToHash) {
|
for (final asset in assetsToHash) {
|
||||||
|
|
@ -98,16 +89,16 @@ class HashService {
|
||||||
|
|
||||||
toHash[asset.id] = asset;
|
toHash[asset.id] = asset;
|
||||||
if (toHash.length == _batchSize) {
|
if (toHash.length == _batchSize) {
|
||||||
await _processBatch(album, toHash);
|
await _processBatch(album, toHash, isTrashed);
|
||||||
toHash.clear();
|
toHash.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await _processBatch(album, toHash);
|
await _processBatch(album, toHash, isTrashed);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Processes a batch of assets.
|
/// 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) {
|
if (toHash.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -142,64 +133,10 @@ class HashService {
|
||||||
}
|
}
|
||||||
|
|
||||||
_log.fine("Hashed ${hashed.length}/${toHash.length} assets");
|
_log.fine("Hashed ${hashed.length}/${toHash.length} assets");
|
||||||
|
if (isTrashed) {
|
||||||
await _localAssetRepository.updateHashes(hashed);
|
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:flutter/foundation.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/local_album.model.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/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/extensions/platform_extensions.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.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/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/datetime_helpers.dart';
|
||||||
import 'package:immich_mobile/utils/diff.dart';
|
import 'package:immich_mobile/utils/diff.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
@ -15,15 +17,18 @@ import 'package:logging/logging.dart';
|
||||||
class LocalSyncService {
|
class LocalSyncService {
|
||||||
final DriftLocalAlbumRepository _localAlbumRepository;
|
final DriftLocalAlbumRepository _localAlbumRepository;
|
||||||
final NativeSyncApi _nativeSyncApi;
|
final NativeSyncApi _nativeSyncApi;
|
||||||
final TrashSyncService _trashSyncService;
|
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
||||||
|
final LocalFilesManagerRepository _localFilesManager;
|
||||||
final Logger _log = Logger("DeviceSyncService");
|
final Logger _log = Logger("DeviceSyncService");
|
||||||
|
|
||||||
LocalSyncService({
|
LocalSyncService({
|
||||||
required DriftLocalAlbumRepository localAlbumRepository,
|
required DriftLocalAlbumRepository localAlbumRepository,
|
||||||
required TrashSyncService trashSyncService,
|
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
|
||||||
|
required LocalFilesManagerRepository localFilesManager,
|
||||||
required NativeSyncApi nativeSyncApi,
|
required NativeSyncApi nativeSyncApi,
|
||||||
}) : _localAlbumRepository = localAlbumRepository,
|
}) : _localAlbumRepository = localAlbumRepository,
|
||||||
_trashSyncService = trashSyncService,
|
_trashedLocalAssetRepository = trashedLocalAssetRepository,
|
||||||
|
_localFilesManager = localFilesManager,
|
||||||
_nativeSyncApi = nativeSyncApi;
|
_nativeSyncApi = nativeSyncApi;
|
||||||
|
|
||||||
Future<void> sync({bool full = false}) async {
|
Future<void> sync({bool full = false}) async {
|
||||||
|
|
@ -34,6 +39,12 @@ class LocalSyncService {
|
||||||
return await fullSync();
|
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();
|
final delta = await _nativeSyncApi.getMediaChanges();
|
||||||
if (!delta.hasChanges) {
|
if (!delta.hasChanges) {
|
||||||
_log.fine("No media changes detected. Skipping sync");
|
_log.fine("No media changes detected. Skipping sync");
|
||||||
|
|
@ -75,11 +86,6 @@ class LocalSyncService {
|
||||||
await updateAlbum(dbAlbum, album);
|
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();
|
await _nativeSyncApi.checkpointSync();
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
_log.severe("Error performing device sync", e, s);
|
_log.severe("Error performing device sync", e, s);
|
||||||
|
|
@ -93,6 +99,10 @@ class LocalSyncService {
|
||||||
try {
|
try {
|
||||||
final Stopwatch stopwatch = Stopwatch()..start();
|
final Stopwatch stopwatch = Stopwatch()..start();
|
||||||
|
|
||||||
|
if (CurrentPlatform.isAndroid) {
|
||||||
|
await _syncDeviceTrashSnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
final deviceAlbums = await _nativeSyncApi.getAlbums();
|
final deviceAlbums = await _nativeSyncApi.getAlbums();
|
||||||
final dbAlbums = await _localAlbumRepository.getAll(sortBy: {SortLocalAlbumsBy.id});
|
final dbAlbums = await _localAlbumRepository.getAll(sortBy: {SortLocalAlbumsBy.id});
|
||||||
|
|
||||||
|
|
@ -104,9 +114,6 @@ class LocalSyncService {
|
||||||
onlyFirst: removeAlbum,
|
onlyFirst: removeAlbum,
|
||||||
onlySecond: addAlbum,
|
onlySecond: addAlbum,
|
||||||
);
|
);
|
||||||
if (_trashSyncService.isAutoSyncMode) {
|
|
||||||
await _trashSyncService.syncDeviceTrashSnapshot();
|
|
||||||
}
|
|
||||||
|
|
||||||
await _nativeSyncApi.checkpointSync();
|
await _nativeSyncApi.checkpointSync();
|
||||||
stopwatch.stop();
|
stopwatch.stop();
|
||||||
|
|
@ -286,6 +293,53 @@ class LocalSyncService {
|
||||||
bool _albumsEqual(LocalAlbum a, LocalAlbum b) {
|
bool _albumsEqual(LocalAlbum a, LocalAlbum b) {
|
||||||
return a.name == b.name && a.assetCount == b.assetCount && a.updatedAt.isAtSameMomentAs(b.updatedAt);
|
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> {
|
extension on Iterable<PlatformAlbum> {
|
||||||
|
|
@ -320,3 +374,26 @@ extension on Iterable<PlatformAsset> {
|
||||||
).toList();
|
).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());
|
return _syncStreamRepository.deletePartnerV1(data.cast());
|
||||||
case SyncEntityType.assetV1:
|
case SyncEntityType.assetV1:
|
||||||
final remoteSyncAssets = data.cast<SyncAssetV1>();
|
final remoteSyncAssets = data.cast<SyncAssetV1>();
|
||||||
if (_trashSyncService.isAutoSyncMode) {
|
if (_trashSyncService.isTrashSyncMode) {
|
||||||
await _trashSyncService.handleRemoteTrashed(
|
await _trashSyncService.handleRemoteTrashed(
|
||||||
remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.checksum),
|
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/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/local_asset.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/storage.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/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/repositories/local_files_manager.repository.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/utils/datetime_helpers.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
typedef TrashSyncItem = ({String remoteId, String checksum, DateTime? deletedAt});
|
|
||||||
|
|
||||||
class TrashSyncService {
|
class TrashSyncService {
|
||||||
final AppSettingsService _appSettingsService;
|
final AppSettingsService _appSettingsService;
|
||||||
final NativeSyncApi _nativeSyncApi;
|
|
||||||
final DriftLocalAssetRepository _localAssetRepository;
|
final DriftLocalAssetRepository _localAssetRepository;
|
||||||
final DriftLocalAlbumRepository _localAlbumRepository;
|
|
||||||
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
||||||
final LocalFilesManagerRepository _localFilesManager;
|
final LocalFilesManagerRepository _localFilesManager;
|
||||||
final StorageRepository _storageRepository;
|
final StorageRepository _storageRepository;
|
||||||
|
|
@ -25,66 +16,25 @@ class TrashSyncService {
|
||||||
|
|
||||||
TrashSyncService({
|
TrashSyncService({
|
||||||
required AppSettingsService appSettingsService,
|
required AppSettingsService appSettingsService,
|
||||||
required NativeSyncApi nativeSyncApi,
|
|
||||||
required DriftLocalAssetRepository localAssetRepository,
|
required DriftLocalAssetRepository localAssetRepository,
|
||||||
required DriftLocalAlbumRepository localAlbumRepository,
|
|
||||||
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
|
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
|
||||||
required LocalFilesManagerRepository localFilesManager,
|
required LocalFilesManagerRepository localFilesManager,
|
||||||
required StorageRepository storageRepository,
|
required StorageRepository storageRepository,
|
||||||
}) : _appSettingsService = appSettingsService,
|
}) : _appSettingsService = appSettingsService,
|
||||||
_nativeSyncApi = nativeSyncApi,
|
|
||||||
_localAssetRepository = localAssetRepository,
|
_localAssetRepository = localAssetRepository,
|
||||||
_localAlbumRepository = localAlbumRepository,
|
|
||||||
_trashedLocalAssetRepository = trashedLocalAssetRepository,
|
_trashedLocalAssetRepository = trashedLocalAssetRepository,
|
||||||
_localFilesManager = localFilesManager,
|
_localFilesManager = localFilesManager,
|
||||||
_storageRepository = storageRepository;
|
_storageRepository = storageRepository;
|
||||||
|
|
||||||
bool get isAutoSyncMode =>
|
bool get isTrashSyncMode =>
|
||||||
CurrentPlatform.isAndroid && _appSettingsService.getSetting<bool>(AppSettingsEnum.manageLocalMediaAndroid);
|
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 {
|
Future<void> handleRemoteTrashed(Iterable<String> checksums) async {
|
||||||
if (checksums.isEmpty) {
|
if (checksums.isEmpty) {
|
||||||
return Future.value();
|
return Future.value();
|
||||||
} else {
|
} else {
|
||||||
final localAssetsToTrash = await _localAssetRepository.getBackupSelectedAssetsByAlbum(checksums);
|
final localAssetsToTrash = await _localAssetRepository.getAssetsFromBackupAlbums(checksums);
|
||||||
if (localAssetsToTrash.isNotEmpty) {
|
if (localAssetsToTrash.isNotEmpty) {
|
||||||
final mediaUrls = await Future.wait(
|
final mediaUrls = await Future.wait(
|
||||||
localAssetsToTrash.values
|
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 albumId => text()();
|
||||||
|
|
||||||
TextColumn get volume => text().nullable()();
|
|
||||||
|
|
||||||
TextColumn get checksum => text().nullable()();
|
TextColumn get checksum => text().nullable()();
|
||||||
|
|
||||||
BoolColumn get isFavorite => boolean().withDefault(const Constant(false))();
|
BoolColumn get isFavorite => boolean().withDefault(const Constant(false))();
|
||||||
|
|
@ -28,7 +26,6 @@ extension TrashedLocalAssetEntityDataDomainExtension on TrashedLocalAssetEntityD
|
||||||
TrashedAsset toDto() => TrashedAsset(
|
TrashedAsset toDto() => TrashedAsset(
|
||||||
id: id,
|
id: id,
|
||||||
name: name,
|
name: name,
|
||||||
volume: volume,
|
|
||||||
albumId: albumId,
|
albumId: albumId,
|
||||||
checksum: checksum,
|
checksum: checksum,
|
||||||
type: type,
|
type: type,
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
// dart format width=80
|
// dart format width=80
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
import 'package:drift/drift.dart' as i0;
|
import 'package:drift/drift.dart' as i0;
|
||||||
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'
|
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
|
||||||
as i1;
|
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as i2;
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' as i2;
|
||||||
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.dart'
|
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.dart'
|
||||||
as i3;
|
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 =
|
typedef $$TrashedLocalAssetEntityTableCreateCompanionBuilder =
|
||||||
i1.TrashedLocalAssetEntityCompanion Function({
|
i1.TrashedLocalAssetEntityCompanion Function({
|
||||||
|
|
@ -19,7 +19,6 @@ typedef $$TrashedLocalAssetEntityTableCreateCompanionBuilder =
|
||||||
i0.Value<int?> durationInSeconds,
|
i0.Value<int?> durationInSeconds,
|
||||||
required String id,
|
required String id,
|
||||||
required String albumId,
|
required String albumId,
|
||||||
i0.Value<String?> volume,
|
|
||||||
i0.Value<String?> checksum,
|
i0.Value<String?> checksum,
|
||||||
i0.Value<bool> isFavorite,
|
i0.Value<bool> isFavorite,
|
||||||
i0.Value<int> orientation,
|
i0.Value<int> orientation,
|
||||||
|
|
@ -35,7 +34,6 @@ typedef $$TrashedLocalAssetEntityTableUpdateCompanionBuilder =
|
||||||
i0.Value<int?> durationInSeconds,
|
i0.Value<int?> durationInSeconds,
|
||||||
i0.Value<String> id,
|
i0.Value<String> id,
|
||||||
i0.Value<String> albumId,
|
i0.Value<String> albumId,
|
||||||
i0.Value<String?> volume,
|
|
||||||
i0.Value<String?> checksum,
|
i0.Value<String?> checksum,
|
||||||
i0.Value<bool> isFavorite,
|
i0.Value<bool> isFavorite,
|
||||||
i0.Value<int> orientation,
|
i0.Value<int> orientation,
|
||||||
|
|
@ -97,11 +95,6 @@ class $$TrashedLocalAssetEntityTableFilterComposer
|
||||||
builder: (column) => i0.ColumnFilters(column),
|
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(
|
i0.ColumnFilters<String> get checksum => $composableBuilder(
|
||||||
column: $table.checksum,
|
column: $table.checksum,
|
||||||
builder: (column) => i0.ColumnFilters(column),
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
|
|
@ -173,11 +166,6 @@ class $$TrashedLocalAssetEntityTableOrderingComposer
|
||||||
builder: (column) => i0.ColumnOrderings(column),
|
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(
|
i0.ColumnOrderings<String> get checksum => $composableBuilder(
|
||||||
column: $table.checksum,
|
column: $table.checksum,
|
||||||
builder: (column) => i0.ColumnOrderings(column),
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
|
|
@ -233,9 +221,6 @@ class $$TrashedLocalAssetEntityTableAnnotationComposer
|
||||||
i0.GeneratedColumn<String> get albumId =>
|
i0.GeneratedColumn<String> get albumId =>
|
||||||
$composableBuilder(column: $table.albumId, builder: (column) => column);
|
$composableBuilder(column: $table.albumId, builder: (column) => column);
|
||||||
|
|
||||||
i0.GeneratedColumn<String> get volume =>
|
|
||||||
$composableBuilder(column: $table.volume, builder: (column) => column);
|
|
||||||
|
|
||||||
i0.GeneratedColumn<String> get checksum =>
|
i0.GeneratedColumn<String> get checksum =>
|
||||||
$composableBuilder(column: $table.checksum, builder: (column) => column);
|
$composableBuilder(column: $table.checksum, builder: (column) => column);
|
||||||
|
|
||||||
|
|
@ -305,7 +290,6 @@ class $$TrashedLocalAssetEntityTableTableManager
|
||||||
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
|
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
|
||||||
i0.Value<String> id = const i0.Value.absent(),
|
i0.Value<String> id = const i0.Value.absent(),
|
||||||
i0.Value<String> albumId = 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<String?> checksum = const i0.Value.absent(),
|
||||||
i0.Value<bool> isFavorite = const i0.Value.absent(),
|
i0.Value<bool> isFavorite = const i0.Value.absent(),
|
||||||
i0.Value<int> orientation = const i0.Value.absent(),
|
i0.Value<int> orientation = const i0.Value.absent(),
|
||||||
|
|
@ -319,7 +303,6 @@ class $$TrashedLocalAssetEntityTableTableManager
|
||||||
durationInSeconds: durationInSeconds,
|
durationInSeconds: durationInSeconds,
|
||||||
id: id,
|
id: id,
|
||||||
albumId: albumId,
|
albumId: albumId,
|
||||||
volume: volume,
|
|
||||||
checksum: checksum,
|
checksum: checksum,
|
||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
orientation: orientation,
|
orientation: orientation,
|
||||||
|
|
@ -335,7 +318,6 @@ class $$TrashedLocalAssetEntityTableTableManager
|
||||||
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
|
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
|
||||||
required String id,
|
required String id,
|
||||||
required String albumId,
|
required String albumId,
|
||||||
i0.Value<String?> volume = const i0.Value.absent(),
|
|
||||||
i0.Value<String?> checksum = const i0.Value.absent(),
|
i0.Value<String?> checksum = const i0.Value.absent(),
|
||||||
i0.Value<bool> isFavorite = const i0.Value.absent(),
|
i0.Value<bool> isFavorite = const i0.Value.absent(),
|
||||||
i0.Value<int> orientation = const i0.Value.absent(),
|
i0.Value<int> orientation = const i0.Value.absent(),
|
||||||
|
|
@ -349,7 +331,6 @@ class $$TrashedLocalAssetEntityTableTableManager
|
||||||
durationInSeconds: durationInSeconds,
|
durationInSeconds: durationInSeconds,
|
||||||
id: id,
|
id: id,
|
||||||
albumId: albumId,
|
albumId: albumId,
|
||||||
volume: volume,
|
|
||||||
checksum: checksum,
|
checksum: checksum,
|
||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
orientation: orientation,
|
orientation: orientation,
|
||||||
|
|
@ -499,17 +480,6 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
|
||||||
type: i0.DriftSqlType.string,
|
type: i0.DriftSqlType.string,
|
||||||
requiredDuringInsert: true,
|
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(
|
static const i0.VerificationMeta _checksumMeta = const i0.VerificationMeta(
|
||||||
'checksum',
|
'checksum',
|
||||||
);
|
);
|
||||||
|
|
@ -559,7 +529,6 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
|
||||||
durationInSeconds,
|
durationInSeconds,
|
||||||
id,
|
id,
|
||||||
albumId,
|
albumId,
|
||||||
volume,
|
|
||||||
checksum,
|
checksum,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
orientation,
|
orientation,
|
||||||
|
|
@ -630,12 +599,6 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
|
||||||
} else if (isInserting) {
|
} else if (isInserting) {
|
||||||
context.missing(_albumIdMeta);
|
context.missing(_albumIdMeta);
|
||||||
}
|
}
|
||||||
if (data.containsKey('volume')) {
|
|
||||||
context.handle(
|
|
||||||
_volumeMeta,
|
|
||||||
volume.isAcceptableOrUnknown(data['volume']!, _volumeMeta),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (data.containsKey('checksum')) {
|
if (data.containsKey('checksum')) {
|
||||||
context.handle(
|
context.handle(
|
||||||
_checksumMeta,
|
_checksumMeta,
|
||||||
|
|
@ -707,10 +670,6 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
|
||||||
i0.DriftSqlType.string,
|
i0.DriftSqlType.string,
|
||||||
data['${effectivePrefix}album_id'],
|
data['${effectivePrefix}album_id'],
|
||||||
)!,
|
)!,
|
||||||
volume: attachedDatabase.typeMapping.read(
|
|
||||||
i0.DriftSqlType.string,
|
|
||||||
data['${effectivePrefix}volume'],
|
|
||||||
),
|
|
||||||
checksum: attachedDatabase.typeMapping.read(
|
checksum: attachedDatabase.typeMapping.read(
|
||||||
i0.DriftSqlType.string,
|
i0.DriftSqlType.string,
|
||||||
data['${effectivePrefix}checksum'],
|
data['${effectivePrefix}checksum'],
|
||||||
|
|
@ -750,7 +709,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||||
final int? durationInSeconds;
|
final int? durationInSeconds;
|
||||||
final String id;
|
final String id;
|
||||||
final String albumId;
|
final String albumId;
|
||||||
final String? volume;
|
|
||||||
final String? checksum;
|
final String? checksum;
|
||||||
final bool isFavorite;
|
final bool isFavorite;
|
||||||
final int orientation;
|
final int orientation;
|
||||||
|
|
@ -764,7 +722,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||||
this.durationInSeconds,
|
this.durationInSeconds,
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.albumId,
|
required this.albumId,
|
||||||
this.volume,
|
|
||||||
this.checksum,
|
this.checksum,
|
||||||
required this.isFavorite,
|
required this.isFavorite,
|
||||||
required this.orientation,
|
required this.orientation,
|
||||||
|
|
@ -791,9 +748,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||||
}
|
}
|
||||||
map['id'] = i0.Variable<String>(id);
|
map['id'] = i0.Variable<String>(id);
|
||||||
map['album_id'] = i0.Variable<String>(albumId);
|
map['album_id'] = i0.Variable<String>(albumId);
|
||||||
if (!nullToAbsent || volume != null) {
|
|
||||||
map['volume'] = i0.Variable<String>(volume);
|
|
||||||
}
|
|
||||||
if (!nullToAbsent || checksum != null) {
|
if (!nullToAbsent || checksum != null) {
|
||||||
map['checksum'] = i0.Variable<String>(checksum);
|
map['checksum'] = i0.Variable<String>(checksum);
|
||||||
}
|
}
|
||||||
|
|
@ -819,7 +773,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||||
durationInSeconds: serializer.fromJson<int?>(json['durationInSeconds']),
|
durationInSeconds: serializer.fromJson<int?>(json['durationInSeconds']),
|
||||||
id: serializer.fromJson<String>(json['id']),
|
id: serializer.fromJson<String>(json['id']),
|
||||||
albumId: serializer.fromJson<String>(json['albumId']),
|
albumId: serializer.fromJson<String>(json['albumId']),
|
||||||
volume: serializer.fromJson<String?>(json['volume']),
|
|
||||||
checksum: serializer.fromJson<String?>(json['checksum']),
|
checksum: serializer.fromJson<String?>(json['checksum']),
|
||||||
isFavorite: serializer.fromJson<bool>(json['isFavorite']),
|
isFavorite: serializer.fromJson<bool>(json['isFavorite']),
|
||||||
orientation: serializer.fromJson<int>(json['orientation']),
|
orientation: serializer.fromJson<int>(json['orientation']),
|
||||||
|
|
@ -840,7 +793,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||||
'durationInSeconds': serializer.toJson<int?>(durationInSeconds),
|
'durationInSeconds': serializer.toJson<int?>(durationInSeconds),
|
||||||
'id': serializer.toJson<String>(id),
|
'id': serializer.toJson<String>(id),
|
||||||
'albumId': serializer.toJson<String>(albumId),
|
'albumId': serializer.toJson<String>(albumId),
|
||||||
'volume': serializer.toJson<String?>(volume),
|
|
||||||
'checksum': serializer.toJson<String?>(checksum),
|
'checksum': serializer.toJson<String?>(checksum),
|
||||||
'isFavorite': serializer.toJson<bool>(isFavorite),
|
'isFavorite': serializer.toJson<bool>(isFavorite),
|
||||||
'orientation': serializer.toJson<int>(orientation),
|
'orientation': serializer.toJson<int>(orientation),
|
||||||
|
|
@ -857,7 +809,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||||
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
|
i0.Value<int?> durationInSeconds = const i0.Value.absent(),
|
||||||
String? id,
|
String? id,
|
||||||
String? albumId,
|
String? albumId,
|
||||||
i0.Value<String?> volume = const i0.Value.absent(),
|
|
||||||
i0.Value<String?> checksum = const i0.Value.absent(),
|
i0.Value<String?> checksum = const i0.Value.absent(),
|
||||||
bool? isFavorite,
|
bool? isFavorite,
|
||||||
int? orientation,
|
int? orientation,
|
||||||
|
|
@ -873,7 +824,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||||
: this.durationInSeconds,
|
: this.durationInSeconds,
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
albumId: albumId ?? this.albumId,
|
albumId: albumId ?? this.albumId,
|
||||||
volume: volume.present ? volume.value : this.volume,
|
|
||||||
checksum: checksum.present ? checksum.value : this.checksum,
|
checksum: checksum.present ? checksum.value : this.checksum,
|
||||||
isFavorite: isFavorite ?? this.isFavorite,
|
isFavorite: isFavorite ?? this.isFavorite,
|
||||||
orientation: orientation ?? this.orientation,
|
orientation: orientation ?? this.orientation,
|
||||||
|
|
@ -893,7 +843,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||||
: this.durationInSeconds,
|
: this.durationInSeconds,
|
||||||
id: data.id.present ? data.id.value : this.id,
|
id: data.id.present ? data.id.value : this.id,
|
||||||
albumId: data.albumId.present ? data.albumId.value : this.albumId,
|
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,
|
checksum: data.checksum.present ? data.checksum.value : this.checksum,
|
||||||
isFavorite: data.isFavorite.present
|
isFavorite: data.isFavorite.present
|
||||||
? data.isFavorite.value
|
? data.isFavorite.value
|
||||||
|
|
@ -916,7 +865,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||||
..write('durationInSeconds: $durationInSeconds, ')
|
..write('durationInSeconds: $durationInSeconds, ')
|
||||||
..write('id: $id, ')
|
..write('id: $id, ')
|
||||||
..write('albumId: $albumId, ')
|
..write('albumId: $albumId, ')
|
||||||
..write('volume: $volume, ')
|
|
||||||
..write('checksum: $checksum, ')
|
..write('checksum: $checksum, ')
|
||||||
..write('isFavorite: $isFavorite, ')
|
..write('isFavorite: $isFavorite, ')
|
||||||
..write('orientation: $orientation')
|
..write('orientation: $orientation')
|
||||||
|
|
@ -935,7 +883,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||||
durationInSeconds,
|
durationInSeconds,
|
||||||
id,
|
id,
|
||||||
albumId,
|
albumId,
|
||||||
volume,
|
|
||||||
checksum,
|
checksum,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
orientation,
|
orientation,
|
||||||
|
|
@ -953,7 +900,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||||
other.durationInSeconds == this.durationInSeconds &&
|
other.durationInSeconds == this.durationInSeconds &&
|
||||||
other.id == this.id &&
|
other.id == this.id &&
|
||||||
other.albumId == this.albumId &&
|
other.albumId == this.albumId &&
|
||||||
other.volume == this.volume &&
|
|
||||||
other.checksum == this.checksum &&
|
other.checksum == this.checksum &&
|
||||||
other.isFavorite == this.isFavorite &&
|
other.isFavorite == this.isFavorite &&
|
||||||
other.orientation == this.orientation);
|
other.orientation == this.orientation);
|
||||||
|
|
@ -970,7 +916,6 @@ class TrashedLocalAssetEntityCompanion
|
||||||
final i0.Value<int?> durationInSeconds;
|
final i0.Value<int?> durationInSeconds;
|
||||||
final i0.Value<String> id;
|
final i0.Value<String> id;
|
||||||
final i0.Value<String> albumId;
|
final i0.Value<String> albumId;
|
||||||
final i0.Value<String?> volume;
|
|
||||||
final i0.Value<String?> checksum;
|
final i0.Value<String?> checksum;
|
||||||
final i0.Value<bool> isFavorite;
|
final i0.Value<bool> isFavorite;
|
||||||
final i0.Value<int> orientation;
|
final i0.Value<int> orientation;
|
||||||
|
|
@ -984,7 +929,6 @@ class TrashedLocalAssetEntityCompanion
|
||||||
this.durationInSeconds = const i0.Value.absent(),
|
this.durationInSeconds = const i0.Value.absent(),
|
||||||
this.id = const i0.Value.absent(),
|
this.id = const i0.Value.absent(),
|
||||||
this.albumId = const i0.Value.absent(),
|
this.albumId = const i0.Value.absent(),
|
||||||
this.volume = const i0.Value.absent(),
|
|
||||||
this.checksum = const i0.Value.absent(),
|
this.checksum = const i0.Value.absent(),
|
||||||
this.isFavorite = const i0.Value.absent(),
|
this.isFavorite = const i0.Value.absent(),
|
||||||
this.orientation = const i0.Value.absent(),
|
this.orientation = const i0.Value.absent(),
|
||||||
|
|
@ -999,7 +943,6 @@ class TrashedLocalAssetEntityCompanion
|
||||||
this.durationInSeconds = const i0.Value.absent(),
|
this.durationInSeconds = const i0.Value.absent(),
|
||||||
required String id,
|
required String id,
|
||||||
required String albumId,
|
required String albumId,
|
||||||
this.volume = const i0.Value.absent(),
|
|
||||||
this.checksum = const i0.Value.absent(),
|
this.checksum = const i0.Value.absent(),
|
||||||
this.isFavorite = const i0.Value.absent(),
|
this.isFavorite = const i0.Value.absent(),
|
||||||
this.orientation = const i0.Value.absent(),
|
this.orientation = const i0.Value.absent(),
|
||||||
|
|
@ -1017,7 +960,6 @@ class TrashedLocalAssetEntityCompanion
|
||||||
i0.Expression<int>? durationInSeconds,
|
i0.Expression<int>? durationInSeconds,
|
||||||
i0.Expression<String>? id,
|
i0.Expression<String>? id,
|
||||||
i0.Expression<String>? albumId,
|
i0.Expression<String>? albumId,
|
||||||
i0.Expression<String>? volume,
|
|
||||||
i0.Expression<String>? checksum,
|
i0.Expression<String>? checksum,
|
||||||
i0.Expression<bool>? isFavorite,
|
i0.Expression<bool>? isFavorite,
|
||||||
i0.Expression<int>? orientation,
|
i0.Expression<int>? orientation,
|
||||||
|
|
@ -1032,7 +974,6 @@ class TrashedLocalAssetEntityCompanion
|
||||||
if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds,
|
if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds,
|
||||||
if (id != null) 'id': id,
|
if (id != null) 'id': id,
|
||||||
if (albumId != null) 'album_id': albumId,
|
if (albumId != null) 'album_id': albumId,
|
||||||
if (volume != null) 'volume': volume,
|
|
||||||
if (checksum != null) 'checksum': checksum,
|
if (checksum != null) 'checksum': checksum,
|
||||||
if (isFavorite != null) 'is_favorite': isFavorite,
|
if (isFavorite != null) 'is_favorite': isFavorite,
|
||||||
if (orientation != null) 'orientation': orientation,
|
if (orientation != null) 'orientation': orientation,
|
||||||
|
|
@ -1049,7 +990,6 @@ class TrashedLocalAssetEntityCompanion
|
||||||
i0.Value<int?>? durationInSeconds,
|
i0.Value<int?>? durationInSeconds,
|
||||||
i0.Value<String>? id,
|
i0.Value<String>? id,
|
||||||
i0.Value<String>? albumId,
|
i0.Value<String>? albumId,
|
||||||
i0.Value<String?>? volume,
|
|
||||||
i0.Value<String?>? checksum,
|
i0.Value<String?>? checksum,
|
||||||
i0.Value<bool>? isFavorite,
|
i0.Value<bool>? isFavorite,
|
||||||
i0.Value<int>? orientation,
|
i0.Value<int>? orientation,
|
||||||
|
|
@ -1064,7 +1004,6 @@ class TrashedLocalAssetEntityCompanion
|
||||||
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
|
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
albumId: albumId ?? this.albumId,
|
albumId: albumId ?? this.albumId,
|
||||||
volume: volume ?? this.volume,
|
|
||||||
checksum: checksum ?? this.checksum,
|
checksum: checksum ?? this.checksum,
|
||||||
isFavorite: isFavorite ?? this.isFavorite,
|
isFavorite: isFavorite ?? this.isFavorite,
|
||||||
orientation: orientation ?? this.orientation,
|
orientation: orientation ?? this.orientation,
|
||||||
|
|
@ -1103,9 +1042,6 @@ class TrashedLocalAssetEntityCompanion
|
||||||
if (albumId.present) {
|
if (albumId.present) {
|
||||||
map['album_id'] = i0.Variable<String>(albumId.value);
|
map['album_id'] = i0.Variable<String>(albumId.value);
|
||||||
}
|
}
|
||||||
if (volume.present) {
|
|
||||||
map['volume'] = i0.Variable<String>(volume.value);
|
|
||||||
}
|
|
||||||
if (checksum.present) {
|
if (checksum.present) {
|
||||||
map['checksum'] = i0.Variable<String>(checksum.value);
|
map['checksum'] = i0.Variable<String>(checksum.value);
|
||||||
}
|
}
|
||||||
|
|
@ -1130,7 +1066,6 @@ class TrashedLocalAssetEntityCompanion
|
||||||
..write('durationInSeconds: $durationInSeconds, ')
|
..write('durationInSeconds: $durationInSeconds, ')
|
||||||
..write('id: $id, ')
|
..write('id: $id, ')
|
||||||
..write('albumId: $albumId, ')
|
..write('albumId: $albumId, ')
|
||||||
..write('volume: $volume, ')
|
|
||||||
..write('checksum: $checksum, ')
|
..write('checksum: $checksum, ')
|
||||||
..write('isFavorite: $isFavorite, ')
|
..write('isFavorite: $isFavorite, ')
|
||||||
..write('orientation: $orientation')
|
..write('orientation: $orientation')
|
||||||
|
|
|
||||||
|
|
@ -100,26 +100,27 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
||||||
return query.map((localAlbum) => localAlbum.toDto()).get();
|
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) {
|
if (checksums.isEmpty) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
final lAlbumAsset = _db.localAlbumAssetEntity;
|
|
||||||
final lAlbum = _db.localAlbumEntity;
|
|
||||||
final lAsset = _db.localAssetEntity;
|
|
||||||
|
|
||||||
final result = <String, List<LocalAsset>>{};
|
final result = <String, List<LocalAsset>>{};
|
||||||
|
|
||||||
for (final slice in checksums.toSet().slices(800)) {
|
for (final slice in checksums.toSet().slices(32000)) {
|
||||||
final rows = await (_db.select(lAlbumAsset).join([
|
final rows =
|
||||||
innerJoin(lAlbum, lAlbumAsset.albumId.equalsExp(lAlbum.id)),
|
await (_db.select(_db.localAlbumAssetEntity).join([
|
||||||
innerJoin(lAsset, lAlbumAsset.assetId.equalsExp(lAsset.id)),
|
innerJoin(_db.localAlbumEntity, _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id)),
|
||||||
])..where(lAlbum.backupSelection.equalsValue(BackupSelection.selected) & lAsset.checksum.isIn(slice))).get();
|
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) {
|
for (final row in rows) {
|
||||||
final albumId = row.readTable(lAlbumAsset).albumId;
|
final albumId = row.readTable(_db.localAlbumAssetEntity).albumId;
|
||||||
final assetData = row.readTable(lAsset);
|
final assetData = row.readTable(_db.localAssetEntity);
|
||||||
final asset = assetData.toDto();
|
final asset = assetData.toDto();
|
||||||
(result[albumId] ??= <LocalAsset>[]).add(asset);
|
(result[albumId] ??= <LocalAsset>[]).add(asset);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,41 +14,45 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
||||||
|
|
||||||
const DriftTrashedLocalAssetRepository(this._db) : super(_db);
|
const DriftTrashedLocalAssetRepository(this._db) : super(_db);
|
||||||
|
|
||||||
Future<void> updateChecksums(Iterable<TrashedAsset> assets) {
|
Future<void> updateHashes(Map<String, String> hashes) {
|
||||||
if (assets.isEmpty) {
|
if (hashes.isEmpty) {
|
||||||
return Future.value();
|
return Future.value();
|
||||||
}
|
}
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
return _db.batch((batch) async {
|
return _db.batch((batch) async {
|
||||||
for (final asset in assets) {
|
for (final entry in hashes.entries) {
|
||||||
batch.update(
|
batch.update(
|
||||||
_db.trashedLocalAssetEntity,
|
_db.trashedLocalAssetEntity,
|
||||||
TrashedLocalAssetEntityCompanion(checksum: Value(asset.checksum), updatedAt: Value(now)),
|
TrashedLocalAssetEntityCompanion(checksum: Value(entry.value), updatedAt: Value(now)),
|
||||||
where: (e) => e.id.equals(asset.id),
|
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());
|
final query = _db.trashedLocalAssetEntity.select()..where((r) => r.albumId.isIn(albumIds) & r.checksum.isNull());
|
||||||
return query.map((row) => row.toDto()).get();
|
return query.map((row) => row.toDto()).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Iterable<TrashedAsset>> getToRestore() async {
|
Future<Iterable<TrashedAsset>> getToRestore() async {
|
||||||
final trashed = _db.trashedLocalAssetEntity;
|
final selectedAlbumIds = (_db.selectOnly(_db.localAlbumEntity)
|
||||||
final remote = _db.remoteAssetEntity;
|
..addColumns([_db.localAlbumEntity.id])
|
||||||
final album = _db.localAlbumEntity;
|
..where(_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected)));
|
||||||
|
|
||||||
final selectedAlbumIds = (_db.selectOnly(album)
|
final rows =
|
||||||
..addColumns([album.id])
|
await (_db.select(_db.trashedLocalAssetEntity).join([
|
||||||
..where(album.backupSelection.equalsValue(BackupSelection.selected)));
|
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([
|
return rows.map((result) => result.readTable(_db.trashedLocalAssetEntity).toDto());
|
||||||
innerJoin(remote, remote.checksum.equalsExp(trashed.checksum)),
|
|
||||||
])..where(trashed.albumId.isInQuery(selectedAlbumIds) & remote.deletedAt.isNull())).get();
|
|
||||||
|
|
||||||
return rows.map((result) => result.readTable(trashed).toDto());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Applies resulted snapshot of trashed assets:
|
/// Applies resulted snapshot of trashed assets:
|
||||||
|
|
@ -61,41 +65,46 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
return _db.transaction(() async {
|
return _db.transaction(() async {
|
||||||
final table = _db.trashedLocalAssetEntity;
|
await _db.batch((batch) {
|
||||||
|
for (final asset in assets) {
|
||||||
|
final companion = TrashedLocalAssetEntityCompanion.insert(
|
||||||
|
id: asset.id,
|
||||||
|
albumId: albumId,
|
||||||
|
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),
|
||||||
|
);
|
||||||
|
|
||||||
final companions = assets.map(
|
batch.insert<$TrashedLocalAssetEntityTable, TrashedLocalAssetEntityData>(
|
||||||
(a) => TrashedLocalAssetEntityCompanion.insert(
|
_db.trashedLocalAssetEntity,
|
||||||
id: a.id,
|
companion,
|
||||||
albumId: albumId,
|
onConflict: DoUpdate((_) => companion, where: (old) => old.updatedAt.isNotValue(asset.updatedAt)),
|
||||||
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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (final slice in companions.slices(400)) {
|
|
||||||
await _db.batch((b) {
|
|
||||||
b.insertAllOnConflictUpdate(table, slice);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
final keepIds = assets.map((asset) => asset.id);
|
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 {
|
} else {
|
||||||
final existingIds = await (_db.selectOnly(table)..addColumns([table.id])).map((r) => r.read(table.id)!).get();
|
final keepIdsSet = keepIds.toSet();
|
||||||
final toDelete = existingIds.where((id) => !keepIds.contains(id));
|
final existingIds = await (_db.selectOnly(
|
||||||
for (final slice in toDelete.slices(400)) {
|
_db.trashedLocalAssetEntity,
|
||||||
await (_db.delete(table)..where((row) => row.id.isIn(slice))).go();
|
)..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) {
|
if (trashUpdates.isEmpty) {
|
||||||
return;
|
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((batch) {
|
||||||
await _db.batch((b) {
|
for (final asset in trashUpdates) {
|
||||||
b.insertAllOnConflictUpdate(_db.trashedLocalAssetEntity, slice);
|
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() {
|
Stream<int> watchCount() {
|
||||||
final t = _db.trashedLocalAssetEntity;
|
return (_db.selectOnly(_db.trashedLocalAssetEntity)..addColumns([_db.trashedLocalAssetEntity.id.count()]))
|
||||||
return (_db.selectOnly(t)..addColumns([t.id.count()])).watchSingle().map((row) => row.read<int>(t.id.count()) ?? 0);
|
.watchSingle()
|
||||||
|
.map((row) => row.read<int>(_db.trashedLocalAssetEntity.id.count()) ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<int> watchHashedCount() {
|
Stream<int> watchHashedCount() {
|
||||||
final t = _db.trashedLocalAssetEntity;
|
return (_db.selectOnly(_db.trashedLocalAssetEntity)
|
||||||
return (_db.selectOnly(t)
|
..addColumns([_db.trashedLocalAssetEntity.id.count()])
|
||||||
..addColumns([t.id.count()])
|
..where(_db.trashedLocalAssetEntity.checksum.isNotNull()))
|
||||||
..where(t.checksum.isNotNull()))
|
|
||||||
.watchSingle()
|
.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 {
|
Future<void> trashLocalAsset(Map<AlbumId, List<LocalAsset>> assetsByAlbums) async {
|
||||||
|
|
@ -150,14 +160,14 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
||||||
final companions = <TrashedLocalAssetEntityCompanion>[];
|
final companions = <TrashedLocalAssetEntityCompanion>[];
|
||||||
final idToDelete = <String>{};
|
final idToDelete = <String>{};
|
||||||
|
|
||||||
assetsByAlbums.forEach((albumId, assets) {
|
for (final entry in assetsByAlbums.entries) {
|
||||||
for (final asset in assets) {
|
for (final asset in entry.value) {
|
||||||
idToDelete.add(asset.id);
|
idToDelete.add(asset.id);
|
||||||
companions.add(
|
companions.add(
|
||||||
TrashedLocalAssetEntityCompanion(
|
TrashedLocalAssetEntityCompanion(
|
||||||
id: Value(asset.id),
|
id: Value(asset.id),
|
||||||
name: Value(asset.name),
|
name: Value(asset.name),
|
||||||
albumId: Value(albumId),
|
albumId: Value(entry.key),
|
||||||
checksum: asset.checksum == null ? const Value.absent() : Value(asset.checksum),
|
checksum: asset.checksum == null ? const Value.absent() : Value(asset.checksum),
|
||||||
type: Value(asset.type),
|
type: Value(asset.type),
|
||||||
width: Value(asset.width),
|
width: Value(asset.width),
|
||||||
|
|
@ -168,17 +178,18 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
await _db.transaction(() async {
|
await _db.transaction(() async {
|
||||||
for (final slice in companions.slices(200)) {
|
await _db.batch((batch) {
|
||||||
await _db.batch((batch) {
|
for (final slice in companions.slices(32000)) {
|
||||||
batch.insertAllOnConflictUpdate(_db.trashedLocalAssetEntity, slice);
|
batch.insertAllOnConflictUpdate(_db.trashedLocalAssetEntity, slice);
|
||||||
});
|
}
|
||||||
}
|
|
||||||
for (final slice in idToDelete.slices(800)) {
|
for (final id in idToDelete) {
|
||||||
await (_db.delete(_db.localAssetEntity)..where((e) => e.id.isIn(slice))).go();
|
batch.deleteWhere(_db.localAssetEntity, (row) => row.id.equals(id));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,8 +223,8 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
||||||
await _db.transaction(() async {
|
await _db.transaction(() async {
|
||||||
await _db.batch((batch) {
|
await _db.batch((batch) {
|
||||||
batch.insertAllOnConflictUpdate(_db.localAssetEntity, localAssets);
|
batch.insertAllOnConflictUpdate(_db.localAssetEntity, localAssets);
|
||||||
for (final slice in ids.slices(32000)) {
|
for (final id in ids) {
|
||||||
batch.deleteWhere(_db.trashedLocalAssetEntity, (tbl) => tbl.id.isIn(slice));
|
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.durationInSeconds,
|
||||||
required this.orientation,
|
required this.orientation,
|
||||||
required this.isFavorite,
|
required this.isFavorite,
|
||||||
this.isTrashed,
|
|
||||||
this.volume,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
String id;
|
String id;
|
||||||
|
|
@ -65,25 +63,8 @@ class PlatformAsset {
|
||||||
|
|
||||||
bool isFavorite;
|
bool isFavorite;
|
||||||
|
|
||||||
bool? isTrashed;
|
|
||||||
|
|
||||||
String? volume;
|
|
||||||
|
|
||||||
List<Object?> _toList() {
|
List<Object?> _toList() {
|
||||||
return <Object?>[
|
return <Object?>[id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite];
|
||||||
id,
|
|
||||||
name,
|
|
||||||
type,
|
|
||||||
createdAt,
|
|
||||||
updatedAt,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
durationInSeconds,
|
|
||||||
orientation,
|
|
||||||
isFavorite,
|
|
||||||
isTrashed,
|
|
||||||
volume,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Object encode() {
|
Object encode() {
|
||||||
|
|
@ -103,8 +84,6 @@ class PlatformAsset {
|
||||||
durationInSeconds: result[7]! as int,
|
durationInSeconds: result[7]! as int,
|
||||||
orientation: result[8]! as int,
|
orientation: result[8]! as int,
|
||||||
isFavorite: result[9]! as bool,
|
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());
|
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 {
|
class _PigeonCodec extends StandardMessageCodec {
|
||||||
const _PigeonCodec();
|
const _PigeonCodec();
|
||||||
@override
|
@override
|
||||||
|
|
@ -323,9 +263,6 @@ class _PigeonCodec extends StandardMessageCodec {
|
||||||
} else if (value is HashResult) {
|
} else if (value is HashResult) {
|
||||||
buffer.putUint8(132);
|
buffer.putUint8(132);
|
||||||
writeValue(buffer, value.encode());
|
writeValue(buffer, value.encode());
|
||||||
} else if (value is TrashedAssetParams) {
|
|
||||||
buffer.putUint8(133);
|
|
||||||
writeValue(buffer, value.encode());
|
|
||||||
} else {
|
} else {
|
||||||
super.writeValue(buffer, value);
|
super.writeValue(buffer, value);
|
||||||
}
|
}
|
||||||
|
|
@ -342,8 +279,6 @@ class _PigeonCodec extends StandardMessageCodec {
|
||||||
return SyncDelta.decode(readValue(buffer)!);
|
return SyncDelta.decode(readValue(buffer)!);
|
||||||
case 132:
|
case 132:
|
||||||
return HashResult.decode(readValue(buffer)!);
|
return HashResult.decode(readValue(buffer)!);
|
||||||
case 133:
|
|
||||||
return TrashedAssetParams.decode(readValue(buffer)!);
|
|
||||||
default:
|
default:
|
||||||
return super.readValueOfType(type, buffer);
|
return super.readValueOfType(type, buffer);
|
||||||
}
|
}
|
||||||
|
|
@ -655,32 +590,4 @@ class NativeSyncApi {
|
||||||
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<PlatformAsset>();
|
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/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/platform.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/providers/infrastructure/trash_sync.provider.dart';
|
||||||
|
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
||||||
|
|
||||||
final syncStreamServiceProvider = Provider(
|
final syncStreamServiceProvider = Provider(
|
||||||
(ref) => SyncStreamService(
|
(ref) => SyncStreamService(
|
||||||
|
|
@ -28,7 +29,8 @@ final syncStreamRepositoryProvider = Provider((ref) => SyncStreamRepository(ref.
|
||||||
final localSyncServiceProvider = Provider(
|
final localSyncServiceProvider = Provider(
|
||||||
(ref) => LocalSyncService(
|
(ref) => LocalSyncService(
|
||||||
localAlbumRepository: ref.watch(localAlbumRepository),
|
localAlbumRepository: ref.watch(localAlbumRepository),
|
||||||
trashSyncService: ref.watch(trashSyncServiceProvider),
|
trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository),
|
||||||
|
localFilesManager: ref.watch(localFilesManagerRepositoryProvider),
|
||||||
nativeSyncApi: ref.watch(nativeSyncApiProvider),
|
nativeSyncApi: ref.watch(nativeSyncApiProvider),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -38,6 +40,6 @@ final hashServiceProvider = Provider(
|
||||||
localAlbumRepository: ref.watch(localAlbumRepository),
|
localAlbumRepository: ref.watch(localAlbumRepository),
|
||||||
localAssetRepository: ref.watch(localAssetRepository),
|
localAssetRepository: ref.watch(localAssetRepository),
|
||||||
nativeSyncApi: ref.watch(nativeSyncApiProvider),
|
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:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/services/trash_sync.service.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/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
||||||
|
|
||||||
import 'asset.provider.dart';
|
|
||||||
|
|
||||||
typedef TrashedAssetsCount = ({int total, int hashed});
|
typedef TrashedAssetsCount = ({int total, int hashed});
|
||||||
|
|
||||||
final trashSyncServiceProvider = Provider(
|
final trashSyncServiceProvider = Provider(
|
||||||
(ref) => TrashSyncService(
|
(ref) => TrashSyncService(
|
||||||
appSettingsService: ref.watch(appSettingsServiceProvider),
|
appSettingsService: ref.watch(appSettingsServiceProvider),
|
||||||
nativeSyncApi: ref.watch(nativeSyncApiProvider),
|
|
||||||
localAssetRepository: ref.watch(localAssetRepository),
|
localAssetRepository: ref.watch(localAssetRepository),
|
||||||
localAlbumRepository: ref.watch(localAlbumRepository),
|
|
||||||
trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository),
|
trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository),
|
||||||
localFilesManager: ref.watch(localFilesManagerRepositoryProvider),
|
localFilesManager: ref.watch(localFilesManagerRepositoryProvider),
|
||||||
storageRepository: ref.watch(storageRepositoryProvider),
|
storageRepository: ref.watch(storageRepositoryProvider),
|
||||||
|
|
|
||||||
|
|
@ -353,7 +353,7 @@ class _SyncStatsCounts extends ConsumerWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (trashSyncService.isAutoSyncMode) ...[
|
if (trashSyncService.isTrashSyncMode) ...[
|
||||||
_SectionHeaderText(text: "trash".t(context: context)),
|
_SectionHeaderText(text: "trash".t(context: context)),
|
||||||
Consumer(
|
Consumer(
|
||||||
builder: (context, ref, _) {
|
builder: (context, ref, _) {
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,10 @@ import 'package:pigeon/pigeon.dart';
|
||||||
class PlatformAsset {
|
class PlatformAsset {
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
|
|
||||||
// Follows AssetType enum from base_asset.model.dart
|
// Follows AssetType enum from base_asset.model.dart
|
||||||
final int type;
|
final int type;
|
||||||
|
|
||||||
// Seconds since epoch
|
// Seconds since epoch
|
||||||
final int? createdAt;
|
final int? createdAt;
|
||||||
final int? updatedAt;
|
final int? updatedAt;
|
||||||
|
|
@ -24,8 +26,6 @@ class PlatformAsset {
|
||||||
final int durationInSeconds;
|
final int durationInSeconds;
|
||||||
final int orientation;
|
final int orientation;
|
||||||
final bool isFavorite;
|
final bool isFavorite;
|
||||||
final bool? isTrashed;
|
|
||||||
final String? volume;
|
|
||||||
|
|
||||||
const PlatformAsset({
|
const PlatformAsset({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
|
@ -38,14 +38,13 @@ class PlatformAsset {
|
||||||
this.durationInSeconds = 0,
|
this.durationInSeconds = 0,
|
||||||
this.orientation = 0,
|
this.orientation = 0,
|
||||||
this.isFavorite = false,
|
this.isFavorite = false,
|
||||||
this.isTrashed,
|
|
||||||
this.volume,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlatformAlbum {
|
class PlatformAlbum {
|
||||||
final String id;
|
final String id;
|
||||||
final String name;
|
final String name;
|
||||||
|
|
||||||
// Seconds since epoch
|
// Seconds since epoch
|
||||||
final int? updatedAt;
|
final int? updatedAt;
|
||||||
final bool isCloud;
|
final bool isCloud;
|
||||||
|
|
@ -64,6 +63,7 @@ class SyncDelta {
|
||||||
final bool hasChanges;
|
final bool hasChanges;
|
||||||
final List<PlatformAsset> updates;
|
final List<PlatformAsset> updates;
|
||||||
final List<String> deletes;
|
final List<String> deletes;
|
||||||
|
|
||||||
// Asset -> Album mapping
|
// Asset -> Album mapping
|
||||||
final Map<String, List<String>> assetAlbums;
|
final Map<String, List<String>> assetAlbums;
|
||||||
|
|
||||||
|
|
@ -83,18 +83,6 @@ class HashResult {
|
||||||
const HashResult({required this.assetId, this.error, this.hash});
|
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()
|
@HostApi()
|
||||||
abstract class NativeSyncApi {
|
abstract class NativeSyncApi {
|
||||||
bool shouldFullSync();
|
bool shouldFullSync();
|
||||||
|
|
@ -126,8 +114,4 @@ abstract class NativeSyncApi {
|
||||||
|
|
||||||
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
|
@TaskQueue(type: TaskQueueType.serialBackgroundThread)
|
||||||
List<PlatformAsset> getTrashedAssetsForAlbum(String albumId);
|
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 MockLocalAlbumRepository mockAlbumRepo;
|
||||||
late MockLocalAssetRepository mockAssetRepo;
|
late MockLocalAssetRepository mockAssetRepo;
|
||||||
late MockNativeSyncApi mockNativeApi;
|
late MockNativeSyncApi mockNativeApi;
|
||||||
late MockTrashSyncService mockTrashSyncService;
|
late MockTrashedLocalAssetRepository mockTrashedAssetRepo;
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
mockAlbumRepo = MockLocalAlbumRepository();
|
mockAlbumRepo = MockLocalAlbumRepository();
|
||||||
mockAssetRepo = MockLocalAssetRepository();
|
mockAssetRepo = MockLocalAssetRepository();
|
||||||
mockNativeApi = MockNativeSyncApi();
|
mockNativeApi = MockNativeSyncApi();
|
||||||
mockTrashSyncService = MockTrashSyncService();
|
mockTrashedAssetRepo = MockTrashedLocalAssetRepository();
|
||||||
|
|
||||||
sut = HashService(
|
sut = HashService(
|
||||||
localAlbumRepository: mockAlbumRepo,
|
localAlbumRepository: mockAlbumRepo,
|
||||||
localAssetRepository: mockAssetRepo,
|
localAssetRepository: mockAssetRepo,
|
||||||
nativeSyncApi: mockNativeApi,
|
nativeSyncApi: mockNativeApi,
|
||||||
trashSyncService: mockTrashSyncService,
|
trashedLocalAssetRepository: mockTrashedAssetRepo,
|
||||||
);
|
);
|
||||||
|
|
||||||
registerFallbackValue(LocalAlbumStub.recent);
|
registerFallbackValue(LocalAlbumStub.recent);
|
||||||
|
|
@ -34,7 +34,6 @@ void main() {
|
||||||
registerFallbackValue(<String, String>{});
|
registerFallbackValue(<String, String>{});
|
||||||
|
|
||||||
when(() => mockAssetRepo.updateHashes(any())).thenAnswer((_) async => {});
|
when(() => mockAssetRepo.updateHashes(any())).thenAnswer((_) async => {});
|
||||||
when(() => mockTrashSyncService.isAutoSyncMode).thenReturn(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
group('HashService hashAssets', () {
|
group('HashService hashAssets', () {
|
||||||
|
|
@ -118,7 +117,7 @@ void main() {
|
||||||
localAssetRepository: mockAssetRepo,
|
localAssetRepository: mockAssetRepo,
|
||||||
nativeSyncApi: mockNativeApi,
|
nativeSyncApi: mockNativeApi,
|
||||||
batchSize: batchSize,
|
batchSize: batchSize,
|
||||||
trashSyncService: mockTrashSyncService,
|
trashedLocalAssetRepository: mockTrashedAssetRepo,
|
||||||
);
|
);
|
||||||
|
|
||||||
final album = LocalAlbumStub.recent;
|
final album = LocalAlbumStub.recent;
|
||||||
|
|
@ -191,4 +190,37 @@ void main() {
|
||||||
verify(() => mockNativeApi.hashAssets([asset2.id], allowNetworkAccess: false)).called(1);
|
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/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/domain/models/exif.model.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart' as old;
|
import 'package:immich_mobile/entities/asset.entity.dart' as old;
|
||||||
|
|
||||||
|
|
@ -74,3 +75,16 @@ abstract final class LocalAssetStub {
|
||||||
updatedAt: DateTime(20021),
|
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/store.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/sync_api.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/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.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/drift_album_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 MockLocalAssetRepository extends Mock implements DriftLocalAssetRepository {}
|
||||||
|
|
||||||
|
class MockTrashedLocalAssetRepository extends Mock implements DriftTrashedLocalAssetRepository {}
|
||||||
|
|
||||||
class MockStorageRepository extends Mock implements StorageRepository {}
|
class MockStorageRepository extends Mock implements StorageRepository {}
|
||||||
|
|
||||||
// API Repos
|
// API Repos
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue