Compare commits

..

4 Commits

Author SHA1 Message Date
shenlong-tanwen
f632e4f666 review changes 2025-09-29 05:46:09 +05:30
shenlong-tanwen
0e8492ceba trailing closures in swift 2025-09-29 03:38:29 +05:30
shenlong-tanwen
13abe14142 async ios 2025-09-29 00:10:34 +05:30
shenlong-tanwen
ae595f2947 fix: android - execute in background 2025-09-29 00:10:34 +05:30
35 changed files with 404 additions and 295 deletions

View File

@@ -1037,7 +1037,6 @@
"exif_bottom_sheet_description_error": "Error updating description", "exif_bottom_sheet_description_error": "Error updating description",
"exif_bottom_sheet_details": "DETAILS", "exif_bottom_sheet_details": "DETAILS",
"exif_bottom_sheet_location": "LOCATION", "exif_bottom_sheet_location": "LOCATION",
"exif_bottom_sheet_no_description": "No description",
"exif_bottom_sheet_people": "PEOPLE", "exif_bottom_sheet_people": "PEOPLE",
"exif_bottom_sheet_person_add_person": "Add name", "exif_bottom_sheet_person_add_person": "Add name",
"exit_slideshow": "Exit Slideshow", "exit_slideshow": "Exit Slideshow",

View File

@@ -3,7 +3,6 @@ package app.alextran.immich
import android.app.Application import android.app.Application
import androidx.work.Configuration import androidx.work.Configuration
import androidx.work.WorkManager import androidx.work.WorkManager
import app.alextran.immich.background.BackgroundWorkerApiImpl
class ImmichApp : Application() { class ImmichApp : Application() {
override fun onCreate() { override fun onCreate() {
@@ -17,6 +16,5 @@ class ImmichApp : Application() {
// As a workaround, we also run a backup check when initializing the application // As a workaround, we also run a backup check when initializing the application
ContentObserverWorker.startBackupWorker(context = this, delayMilliseconds = 0) ContentObserverWorker.startBackupWorker(context = this, delayMilliseconds = 0)
BackgroundWorkerApiImpl.enqueueBackgroundWorker(this)
} }
} }

View File

@@ -0,0 +1,16 @@
package app.alextran.immich
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
fun <T> dispatch(
dispatcher: CoroutineDispatcher = Dispatchers.IO,
callback: (Result<T>) -> Unit,
block: () -> T
) {
CoroutineScope(dispatcher).launch {
callback(runCatching { block() })
}
}

View File

@@ -2,49 +2,50 @@ package app.alextran.immich.background
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import app.alextran.immich.dispatch
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
private const val TAG = "BackgroundEngineLock" private const val TAG = "BackgroundEngineLock"
class BackgroundEngineLock(context: Context) : BackgroundWorkerLockApi, FlutterPlugin { class BackgroundEngineLock(context: Context) : BackgroundWorkerLockApi, FlutterPlugin {
private val ctx: Context = context.applicationContext private val ctx: Context = context.applicationContext
companion object { companion object {
private var engineCount = AtomicInteger(0) private var engineCount = AtomicInteger(0)
private fun checkAndEnforceBackgroundLock(ctx: Context) { private fun checkAndEnforceBackgroundLock(ctx: Context) {
// work manager task is running while the main app is opened, cancel the worker // work manager task is running while the main app is opened, cancel the worker
if (BackgroundWorkerPreferences(ctx).isLocked() && if (BackgroundWorkerPreferences(ctx).isLocked() &&
engineCount.get() > 1 && engineCount.get() > 1 &&
BackgroundWorkerApiImpl.isBackgroundWorkerRunning() BackgroundWorkerApiImpl.isBackgroundWorkerRunning()
) { ) {
Log.i(TAG, "Background worker is locked, cancelling the background worker") Log.i(TAG, "Background worker is locked, cancelling the background worker")
BackgroundWorkerApiImpl.cancelBackgroundWorker(ctx) BackgroundWorkerApiImpl.cancelBackgroundWorker(ctx)
} }
}
} }
}
override fun lock() { override fun lock(callback: (Result<Unit>) -> Unit) = dispatch(callback = callback) {
BackgroundWorkerPreferences(ctx).setLocked(true) BackgroundWorkerPreferences(ctx).setLocked(true)
checkAndEnforceBackgroundLock(ctx) checkAndEnforceBackgroundLock(ctx)
Log.i(TAG, "Background worker is locked") Log.i(TAG, "Background worker is locked")
} }
override fun unlock() { override fun unlock(callback: (Result<Unit>) -> Unit) = dispatch(callback = callback) {
BackgroundWorkerPreferences(ctx).setLocked(false) BackgroundWorkerPreferences(ctx).setLocked(false)
Log.i(TAG, "Background worker is unlocked") Log.i(TAG, "Background worker is unlocked")
} }
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
checkAndEnforceBackgroundLock(binding.applicationContext) checkAndEnforceBackgroundLock(binding.applicationContext)
engineCount.incrementAndGet() engineCount.incrementAndGet()
Log.i(TAG, "Flutter engine attached. Attached Engines count: $engineCount") Log.i(TAG, "Flutter engine attached. Attached Engines count: $engineCount")
} }
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
engineCount.decrementAndGet() engineCount.decrementAndGet()
Log.i(TAG, "Flutter engine detached. Attached Engines count: $engineCount") Log.i(TAG, "Flutter engine detached. Attached Engines count: $engineCount")
} }
} }

View File

@@ -133,11 +133,12 @@ private open class BackgroundWorkerPigeonCodec : StandardMessageCodec() {
} }
} }
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface BackgroundWorkerFgHostApi { interface BackgroundWorkerFgHostApi {
fun enable() fun enable(callback: (Result<Unit>) -> Unit)
fun configure(settings: BackgroundWorkerSettings) fun configure(settings: BackgroundWorkerSettings, callback: (Result<Unit>) -> Unit)
fun disable() fun disable(callback: (Result<Unit>) -> Unit)
companion object { companion object {
/** The codec used by BackgroundWorkerFgHostApi. */ /** The codec used by BackgroundWorkerFgHostApi. */
@@ -152,13 +153,14 @@ interface BackgroundWorkerFgHostApi {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable$separatedMessageChannelSuffix", codec) val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable$separatedMessageChannelSuffix", codec)
if (api != null) { if (api != null) {
channel.setMessageHandler { _, reply -> channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try { api.enable{ result: Result<Unit> ->
api.enable() val error = result.exceptionOrNull()
listOf(null) if (error != null) {
} catch (exception: Throwable) { reply.reply(BackgroundWorkerPigeonUtils.wrapError(error))
BackgroundWorkerPigeonUtils.wrapError(exception) } else {
reply.reply(BackgroundWorkerPigeonUtils.wrapResult(null))
}
} }
reply.reply(wrapped)
} }
} else { } else {
channel.setMessageHandler(null) channel.setMessageHandler(null)
@@ -170,13 +172,14 @@ interface BackgroundWorkerFgHostApi {
channel.setMessageHandler { message, reply -> channel.setMessageHandler { message, reply ->
val args = message as List<Any?> val args = message as List<Any?>
val settingsArg = args[0] as BackgroundWorkerSettings val settingsArg = args[0] as BackgroundWorkerSettings
val wrapped: List<Any?> = try { api.configure(settingsArg) { result: Result<Unit> ->
api.configure(settingsArg) val error = result.exceptionOrNull()
listOf(null) if (error != null) {
} catch (exception: Throwable) { reply.reply(BackgroundWorkerPigeonUtils.wrapError(error))
BackgroundWorkerPigeonUtils.wrapError(exception) } else {
reply.reply(BackgroundWorkerPigeonUtils.wrapResult(null))
}
} }
reply.reply(wrapped)
} }
} else { } else {
channel.setMessageHandler(null) channel.setMessageHandler(null)
@@ -186,13 +189,14 @@ interface BackgroundWorkerFgHostApi {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$separatedMessageChannelSuffix", codec) val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$separatedMessageChannelSuffix", codec)
if (api != null) { if (api != null) {
channel.setMessageHandler { _, reply -> channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try { api.disable{ result: Result<Unit> ->
api.disable() val error = result.exceptionOrNull()
listOf(null) if (error != null) {
} catch (exception: Throwable) { reply.reply(BackgroundWorkerPigeonUtils.wrapError(error))
BackgroundWorkerPigeonUtils.wrapError(exception) } else {
reply.reply(BackgroundWorkerPigeonUtils.wrapResult(null))
}
} }
reply.reply(wrapped)
} }
} else { } else {
channel.setMessageHandler(null) channel.setMessageHandler(null)

View File

@@ -215,7 +215,7 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
if (foregroundFuture != null && !foregroundFuture.isCancelled && !foregroundFuture.isDone) { if (foregroundFuture != null && !foregroundFuture.isCancelled && !foregroundFuture.isDone) {
try { try {
foregroundFuture.get(500, TimeUnit.MILLISECONDS) foregroundFuture.get(500, TimeUnit.MILLISECONDS)
} catch (e: Exception) { } catch (_: Exception) {
// ignored, there is nothing to be done // ignored, there is nothing to be done
} }
} }

View File

@@ -8,6 +8,7 @@ import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager import androidx.work.WorkManager
import app.alextran.immich.dispatch
import io.flutter.embedding.engine.FlutterEngineCache import io.flutter.embedding.engine.FlutterEngineCache
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -16,16 +17,18 @@ private const val TAG = "BackgroundWorkerApiImpl"
class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi { class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
private val ctx: Context = context.applicationContext private val ctx: Context = context.applicationContext
override fun enable() { override fun enable(callback: (Result<Unit>) -> Unit) =
enqueueMediaObserver(ctx) dispatch(callback = callback) { enqueueMediaObserver(ctx) }
}
override fun configure(settings: BackgroundWorkerSettings) { override fun configure(
settings: BackgroundWorkerSettings,
callback: (Result<Unit>) -> Unit
) = dispatch(callback = callback) {
BackgroundWorkerPreferences(ctx).updateSettings(settings) BackgroundWorkerPreferences(ctx).updateSettings(settings)
enqueueMediaObserver(ctx) enqueueMediaObserver(ctx)
} }
override fun disable() { override fun disable(callback: (Result<Unit>) -> Unit) = dispatch(callback = callback) {
WorkManager.getInstance(ctx).apply { WorkManager.getInstance(ctx).apply {
cancelUniqueWork(OBSERVER_WORKER_NAME) cancelUniqueWork(OBSERVER_WORKER_NAME)
cancelUniqueWork(BACKGROUND_WORKER_NAME) cancelUniqueWork(BACKGROUND_WORKER_NAME)
@@ -38,7 +41,6 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
private const val OBSERVER_WORKER_NAME = "immich/MediaObserverV1" private const val OBSERVER_WORKER_NAME = "immich/MediaObserverV1"
const val ENGINE_CACHE_KEY = "immich::background_worker::engine" const val ENGINE_CACHE_KEY = "immich::background_worker::engine"
fun enqueueMediaObserver(ctx: Context) { fun enqueueMediaObserver(ctx: Context) {
val settings = BackgroundWorkerPreferences(ctx).getSettings() val settings = BackgroundWorkerPreferences(ctx).getSettings()
val constraints = Constraints.Builder().apply { val constraints = Constraints.Builder().apply {

View File

@@ -44,10 +44,11 @@ private open class BackgroundWorkerLockPigeonCodec : StandardMessageCodec() {
} }
} }
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface BackgroundWorkerLockApi { interface BackgroundWorkerLockApi {
fun lock() fun lock(callback: (Result<Unit>) -> Unit)
fun unlock() fun unlock(callback: (Result<Unit>) -> Unit)
companion object { companion object {
/** The codec used by BackgroundWorkerLockApi. */ /** The codec used by BackgroundWorkerLockApi. */
@@ -62,13 +63,14 @@ interface BackgroundWorkerLockApi {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.lock$separatedMessageChannelSuffix", codec) val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.lock$separatedMessageChannelSuffix", codec)
if (api != null) { if (api != null) {
channel.setMessageHandler { _, reply -> channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try { api.lock{ result: Result<Unit> ->
api.lock() val error = result.exceptionOrNull()
listOf(null) if (error != null) {
} catch (exception: Throwable) { reply.reply(BackgroundWorkerLockPigeonUtils.wrapError(error))
BackgroundWorkerLockPigeonUtils.wrapError(exception) } else {
reply.reply(BackgroundWorkerLockPigeonUtils.wrapResult(null))
}
} }
reply.reply(wrapped)
} }
} else { } else {
channel.setMessageHandler(null) channel.setMessageHandler(null)
@@ -78,13 +80,14 @@ interface BackgroundWorkerLockApi {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.unlock$separatedMessageChannelSuffix", codec) val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.unlock$separatedMessageChannelSuffix", codec)
if (api != null) { if (api != null) {
channel.setMessageHandler { _, reply -> channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try { api.unlock{ result: Result<Unit> ->
api.unlock() val error = result.exceptionOrNull()
listOf(null) if (error != null) {
} catch (exception: Throwable) { reply.reply(BackgroundWorkerLockPigeonUtils.wrapError(error))
BackgroundWorkerLockPigeonUtils.wrapError(exception) } else {
reply.reply(BackgroundWorkerLockPigeonUtils.wrapResult(null))
}
} }
reply.reply(wrapped)
} }
} else { } else {
channel.setMessageHandler(null) channel.setMessageHandler(null)

View File

@@ -82,9 +82,10 @@ private open class ConnectivityPigeonCodec : StandardMessageCodec() {
} }
} }
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface ConnectivityApi { interface ConnectivityApi {
fun getCapabilities(): List<NetworkCapability> fun getCapabilities(callback: (Result<List<NetworkCapability>>) -> Unit)
companion object { companion object {
/** The codec used by ConnectivityApi. */ /** The codec used by ConnectivityApi. */
@@ -100,12 +101,15 @@ interface ConnectivityApi {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities$separatedMessageChannelSuffix", codec, taskQueue) val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities$separatedMessageChannelSuffix", codec, taskQueue)
if (api != null) { if (api != null) {
channel.setMessageHandler { _, reply -> channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try { api.getCapabilities{ result: Result<List<NetworkCapability>> ->
listOf(api.getCapabilities()) val error = result.exceptionOrNull()
} catch (exception: Throwable) { if (error != null) {
ConnectivityPigeonUtils.wrapError(exception) reply.reply(ConnectivityPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(ConnectivityPigeonUtils.wrapResult(data))
}
} }
reply.reply(wrapped)
} }
} else { } else {
channel.setMessageHandler(null) channel.setMessageHandler(null)

View File

@@ -4,6 +4,7 @@ import android.content.Context
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
import app.alextran.immich.dispatch
class ConnectivityApiImpl(context: Context) : ConnectivityApi { class ConnectivityApiImpl(context: Context) : ConnectivityApi {
private val connectivityManager = private val connectivityManager =
@@ -11,7 +12,13 @@ class ConnectivityApiImpl(context: Context) : ConnectivityApi {
private val wifiManager = private val wifiManager =
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
override fun getCapabilities(): List<NetworkCapability> {
override fun getCapabilities(callback: (Result<List<NetworkCapability>>) -> Unit) =
dispatch(callback = callback) {
getCapabilities()
}
private fun getCapabilities(): List<NetworkCapability> {
val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
?: return emptyList() ?: return emptyList()

View File

@@ -296,13 +296,13 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface NativeSyncApi { interface NativeSyncApi {
fun shouldFullSync(): Boolean fun shouldFullSync(): Boolean
fun getMediaChanges(): SyncDelta fun getMediaChanges(callback: (Result<SyncDelta>) -> Unit)
fun checkpointSync() fun checkpointSync()
fun clearSyncCheckpoint() fun clearSyncCheckpoint()
fun getAssetIdsForAlbum(albumId: String): List<String> fun getAssetIdsForAlbum(albumId: String, callback: (Result<List<String>>) -> Unit)
fun getAlbums(): List<PlatformAlbum> fun getAlbums(callback: (Result<List<PlatformAlbum>>) -> Unit)
fun getAssetsCountSince(albumId: String, timestamp: Long): Long fun getAssetsCountSince(albumId: String, timestamp: Long, callback: (Result<Long>) -> Unit)
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<PlatformAsset> fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?, callback: (Result<List<PlatformAsset>>) -> Unit)
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()
@@ -335,12 +335,15 @@ interface NativeSyncApi {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$separatedMessageChannelSuffix", codec, taskQueue) val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$separatedMessageChannelSuffix", codec, taskQueue)
if (api != null) { if (api != null) {
channel.setMessageHandler { _, reply -> channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try { api.getMediaChanges{ result: Result<SyncDelta> ->
listOf(api.getMediaChanges()) val error = result.exceptionOrNull()
} catch (exception: Throwable) { if (error != null) {
MessagesPigeonUtils.wrapError(exception) reply.reply(MessagesPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(MessagesPigeonUtils.wrapResult(data))
}
} }
reply.reply(wrapped)
} }
} else { } else {
channel.setMessageHandler(null) channel.setMessageHandler(null)
@@ -384,12 +387,15 @@ interface NativeSyncApi {
channel.setMessageHandler { message, reply -> channel.setMessageHandler { message, reply ->
val args = message as List<Any?> val args = message as List<Any?>
val albumIdArg = args[0] as String val albumIdArg = args[0] as String
val wrapped: List<Any?> = try { api.getAssetIdsForAlbum(albumIdArg) { result: Result<List<String>> ->
listOf(api.getAssetIdsForAlbum(albumIdArg)) val error = result.exceptionOrNull()
} catch (exception: Throwable) { if (error != null) {
MessagesPigeonUtils.wrapError(exception) reply.reply(MessagesPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(MessagesPigeonUtils.wrapResult(data))
}
} }
reply.reply(wrapped)
} }
} else { } else {
channel.setMessageHandler(null) channel.setMessageHandler(null)
@@ -399,12 +405,15 @@ interface NativeSyncApi {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$separatedMessageChannelSuffix", codec, taskQueue) val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$separatedMessageChannelSuffix", codec, taskQueue)
if (api != null) { if (api != null) {
channel.setMessageHandler { _, reply -> channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try { api.getAlbums{ result: Result<List<PlatformAlbum>> ->
listOf(api.getAlbums()) val error = result.exceptionOrNull()
} catch (exception: Throwable) { if (error != null) {
MessagesPigeonUtils.wrapError(exception) reply.reply(MessagesPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(MessagesPigeonUtils.wrapResult(data))
}
} }
reply.reply(wrapped)
} }
} else { } else {
channel.setMessageHandler(null) channel.setMessageHandler(null)
@@ -417,12 +426,15 @@ interface NativeSyncApi {
val args = message as List<Any?> val args = message as List<Any?>
val albumIdArg = args[0] as String val albumIdArg = args[0] as String
val timestampArg = args[1] as Long val timestampArg = args[1] as Long
val wrapped: List<Any?> = try { api.getAssetsCountSince(albumIdArg, timestampArg) { result: Result<Long> ->
listOf(api.getAssetsCountSince(albumIdArg, timestampArg)) val error = result.exceptionOrNull()
} catch (exception: Throwable) { if (error != null) {
MessagesPigeonUtils.wrapError(exception) reply.reply(MessagesPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(MessagesPigeonUtils.wrapResult(data))
}
} }
reply.reply(wrapped)
} }
} else { } else {
channel.setMessageHandler(null) channel.setMessageHandler(null)
@@ -435,12 +447,15 @@ interface NativeSyncApi {
val args = message as List<Any?> val args = message as List<Any?>
val albumIdArg = args[0] as String val albumIdArg = args[0] as String
val updatedTimeCondArg = args[1] as Long? val updatedTimeCondArg = args[1] as Long?
val wrapped: List<Any?> = try { api.getAssetsForAlbum(albumIdArg, updatedTimeCondArg) { result: Result<List<PlatformAsset>> ->
listOf(api.getAssetsForAlbum(albumIdArg, updatedTimeCondArg)) val error = result.exceptionOrNull()
} catch (exception: Throwable) { if (error != null) {
MessagesPigeonUtils.wrapError(exception) reply.reply(MessagesPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(MessagesPigeonUtils.wrapResult(data))
}
} }
reply.reply(wrapped)
} }
} else { } else {
channel.setMessageHandler(null) channel.setMessageHandler(null)

View File

@@ -18,7 +18,7 @@ class NativeSyncApiImpl26(context: Context) : NativeSyncApiImplBase(context), Na
// No-op for Android 10 and below // No-op for Android 10 and below
} }
override fun getMediaChanges(): SyncDelta { override fun getMediaChanges(callback: (Result<SyncDelta>) -> Unit) =
throw IllegalStateException("Method not supported on this Android version.") callback(Result.failure(IllegalStateException("Method not supported on this Android version.")))
}
} }

View File

@@ -5,6 +5,7 @@ import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.annotation.RequiresExtension import androidx.annotation.RequiresExtension
import app.alextran.immich.dispatch
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@RequiresApi(Build.VERSION_CODES.Q) @RequiresApi(Build.VERSION_CODES.Q)
@@ -47,7 +48,12 @@ class NativeSyncApiImpl30(context: Context) : NativeSyncApiImplBase(context), Na
} }
} }
override fun getMediaChanges(): SyncDelta { override fun getMediaChanges(callback: (Result<SyncDelta>) -> Unit) =
dispatch(callback = callback) {
getMediaChanges()
}
private fun getMediaChanges(): SyncDelta {
val genMap = getSavedGenerationMap() val genMap = getSavedGenerationMap()
val currentVolumes = MediaStore.getExternalVolumeNames(ctx) val currentVolumes = MediaStore.getExternalVolumeNames(ctx)
val changed = mutableListOf<PlatformAsset>() val changed = mutableListOf<PlatformAsset>()

View File

@@ -7,6 +7,7 @@ import android.database.Cursor
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Base64 import android.util.Base64
import androidx.core.database.getStringOrNull import androidx.core.database.getStringOrNull
import app.alextran.immich.dispatch
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -145,7 +146,10 @@ open class NativeSyncApiImplBase(context: Context) {
} }
} }
fun getAlbums(): List<PlatformAlbum> { fun getAlbums(callback: (Result<List<PlatformAlbum>>) -> Unit) =
dispatch(callback = callback) { getAlbums() }
private fun getAlbums(): List<PlatformAlbum> {
val albums = mutableListOf<PlatformAlbum>() val albums = mutableListOf<PlatformAlbum>()
val albumsCount = mutableMapOf<String, Int>() val albumsCount = mutableMapOf<String, Int>()
@@ -192,7 +196,10 @@ open class NativeSyncApiImplBase(context: Context) {
.sortedBy { it.id } .sortedBy { it.id }
} }
fun getAssetIdsForAlbum(albumId: String): List<String> { fun getAssetIdsForAlbum(albumId: String, callback: (Result<List<String>>) -> Unit) =
dispatch(callback = callback) { getAssetIdsForAlbum(albumId); }
private fun getAssetIdsForAlbum(albumId: String): List<String> {
val projection = arrayOf(MediaStore.MediaColumns._ID) val projection = arrayOf(MediaStore.MediaColumns._ID)
return getCursor( return getCursor(
@@ -208,15 +215,23 @@ open class NativeSyncApiImplBase(context: Context) {
} ?: emptyList() } ?: emptyList()
} }
fun getAssetsCountSince(albumId: String, timestamp: Long): Long = fun getAssetsCountSince(albumId: String, timestamp: Long, callback: (Result<Long>) -> Unit) =
dispatch(callback = callback) { getAssetsCountSince(albumId, timestamp) }
private fun getAssetsCountSince(albumId: String, timestamp: Long): Long =
getCursor( getCursor(
MediaStore.VOLUME_EXTERNAL, MediaStore.VOLUME_EXTERNAL,
"$BUCKET_SELECTION AND ${MediaStore.Files.FileColumns.DATE_ADDED} > ? AND $MEDIA_SELECTION", "$BUCKET_SELECTION AND ${MediaStore.Files.FileColumns.DATE_ADDED} > ? AND $MEDIA_SELECTION",
arrayOf(albumId, timestamp.toString(), *MEDIA_SELECTION_ARGS), arrayOf(albumId, timestamp.toString(), *MEDIA_SELECTION_ARGS),
)?.use { cursor -> cursor.count.toLong() } ?: 0L )?.use { cursor -> cursor.count.toLong() } ?: 0L
fun getAssetsForAlbum(
albumId: String,
updatedTimeCond: Long?,
callback: (Result<List<PlatformAsset>>) -> Unit
) = dispatch(callback = callback) { getAssetsForAlbum(albumId, updatedTimeCond) }
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<PlatformAsset> { private fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<PlatformAsset> {
var selection = "$BUCKET_SELECTION AND $MEDIA_SELECTION" var selection = "$BUCKET_SELECTION AND $MEDIA_SELECTION"
val selectionArgs = mutableListOf(albumId, *MEDIA_SELECTION_ARGS) val selectionArgs = mutableListOf(albumId, *MEDIA_SELECTION_ARGS)
@@ -254,7 +269,7 @@ open class NativeSyncApiImplBase(context: Context) {
}.awaitAll() }.awaitAll()
callback(Result.success(results)) callback(Result.success(results))
} catch (e: CancellationException) { } catch (_: CancellationException) {
callback( callback(
Result.failure( Result.failure(
FlutterError( FlutterError(

View File

@@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 54; objectVersion = 77;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@@ -133,11 +133,14 @@
/* Begin PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFileSystemSynchronizedRootGroup section */
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = { B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
isa = PBXFileSystemSynchronizedRootGroup; isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = Sync; path = Sync;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
B2D27ABE2E84A0FF004DD55B /* Core */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = Core;
sourceTree = "<group>";
};
F0B57D3D2DF764BD00DC5BCC /* WidgetExtension */ = { F0B57D3D2DF764BD00DC5BCC /* WidgetExtension */ = {
isa = PBXFileSystemSynchronizedRootGroup; isa = PBXFileSystemSynchronizedRootGroup;
exceptions = ( exceptions = (
@@ -247,6 +250,7 @@
97C146F01CF9000F007C117D /* Runner */ = { 97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
B2D27ABE2E84A0FF004DD55B /* Core */,
B25D37792E72CA15008B6CA7 /* Connectivity */, B25D37792E72CA15008B6CA7 /* Connectivity */,
B21E34A62E5AF9760031FDB9 /* Background */, B21E34A62E5AF9760031FDB9 /* Background */,
B2CF7F8C2DDE4EBB00744BF6 /* Sync */, B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
@@ -332,6 +336,7 @@
); );
fileSystemSynchronizedGroups = ( fileSystemSynchronizedGroups = (
B2CF7F8C2DDE4EBB00744BF6 /* Sync */, B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
B2D27ABE2E84A0FF004DD55B /* Core */,
); );
name = Runner; name = Runner;
productName = Runner; productName = Runner;
@@ -521,10 +526,14 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Copy Pods Resources"; name = "[CP] Copy Pods Resources";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -553,10 +562,14 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";

View File

@@ -179,11 +179,12 @@ class BackgroundWorkerPigeonCodec: FlutterStandardMessageCodec, @unchecked Senda
static let shared = BackgroundWorkerPigeonCodec(readerWriter: BackgroundWorkerPigeonCodecReaderWriter()) static let shared = BackgroundWorkerPigeonCodec(readerWriter: BackgroundWorkerPigeonCodecReaderWriter())
} }
/// Generated protocol from Pigeon that represents a handler of messages from Flutter. /// Generated protocol from Pigeon that represents a handler of messages from Flutter.
protocol BackgroundWorkerFgHostApi { protocol BackgroundWorkerFgHostApi {
func enable() throws func enable(completion: @escaping (Result<Void, Error>) -> Void)
func configure(settings: BackgroundWorkerSettings) throws func configure(settings: BackgroundWorkerSettings, completion: @escaping (Result<Void, Error>) -> Void)
func disable() throws func disable(completion: @escaping (Result<Void, Error>) -> Void)
} }
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@@ -195,11 +196,13 @@ class BackgroundWorkerFgHostApiSetup {
let enableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) let enableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api { if let api = api {
enableChannel.setMessageHandler { _, reply in enableChannel.setMessageHandler { _, reply in
do { api.enable { result in
try api.enable() switch result {
reply(wrapResult(nil)) case .success:
} catch { reply(wrapResult(nil))
reply(wrapError(error)) case .failure(let error):
reply(wrapError(error))
}
} }
} }
} else { } else {
@@ -210,11 +213,13 @@ class BackgroundWorkerFgHostApiSetup {
configureChannel.setMessageHandler { message, reply in configureChannel.setMessageHandler { message, reply in
let args = message as! [Any?] let args = message as! [Any?]
let settingsArg = args[0] as! BackgroundWorkerSettings let settingsArg = args[0] as! BackgroundWorkerSettings
do { api.configure(settings: settingsArg) { result in
try api.configure(settings: settingsArg) switch result {
reply(wrapResult(nil)) case .success:
} catch { reply(wrapResult(nil))
reply(wrapError(error)) case .failure(let error):
reply(wrapError(error))
}
} }
} }
} else { } else {
@@ -223,11 +228,13 @@ class BackgroundWorkerFgHostApiSetup {
let disableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) let disableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api { if let api = api {
disableChannel.setMessageHandler { _, reply in disableChannel.setMessageHandler { _, reply in
do { api.disable { result in
try api.disable() switch result {
reply(wrapResult(nil)) case .success:
} catch { reply(wrapResult(nil))
reply(wrapError(error)) case .failure(let error):
reply(wrapError(error))
}
} }
} }
} else { } else {

View File

@@ -1,21 +1,25 @@
import BackgroundTasks import BackgroundTasks
class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
func enable(completion: @escaping (Result<Void, any Error>) -> Void) {
func enable() throws { dispatch(completion: completion) {
BackgroundWorkerApiImpl.scheduleRefreshWorker() BackgroundWorkerApiImpl.scheduleRefreshWorker()
BackgroundWorkerApiImpl.scheduleProcessingWorker() BackgroundWorkerApiImpl.scheduleProcessingWorker()
print("BackgroundWorkerApiImpl:enable Background worker scheduled") print("BackgroundWorkerApiImpl:enable Background worker scheduled")
}
} }
func configure(settings: BackgroundWorkerSettings) throws { func configure(settings: BackgroundWorkerSettings, completion: @escaping (Result<Void, any Error>) -> Void) {
// Android only // Android only
completion(Result.success(Void()))
} }
func disable() throws { func disable(completion: @escaping (Result<Void, any Error>) -> Void) {
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.refreshTaskID); dispatch(completion: completion) {
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.processingTaskID); BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.refreshTaskID);
print("BackgroundWorkerApiImpl:disableUploadWorker Disabled background workers") BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.processingTaskID);
print("BackgroundWorkerApiImpl:disableUploadWorker Disabled background workers")
}
} }
private static let refreshTaskID = "app.alextran.immich.background.refreshUpload" private static let refreshTaskID = "app.alextran.immich.background.refreshUpload"

View File

@@ -94,9 +94,10 @@ class ConnectivityPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable
static let shared = ConnectivityPigeonCodec(readerWriter: ConnectivityPigeonCodecReaderWriter()) static let shared = ConnectivityPigeonCodec(readerWriter: ConnectivityPigeonCodecReaderWriter())
} }
/// Generated protocol from Pigeon that represents a handler of messages from Flutter. /// Generated protocol from Pigeon that represents a handler of messages from Flutter.
protocol ConnectivityApi { protocol ConnectivityApi {
func getCapabilities() throws -> [NetworkCapability] func getCapabilities(completion: @escaping (Result<[NetworkCapability], Error>) -> Void)
} }
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@@ -115,11 +116,13 @@ class ConnectivityApiSetup {
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
if let api = api { if let api = api {
getCapabilitiesChannel.setMessageHandler { _, reply in getCapabilitiesChannel.setMessageHandler { _, reply in
do { api.getCapabilities { result in
let result = try api.getCapabilities() switch result {
reply(wrapResult(result)) case .success(let res):
} catch { reply(wrapResult(res))
reply(wrapError(error)) case .failure(let error):
reply(wrapError(error))
}
} }
} }
} else { } else {

View File

@@ -1,6 +1,6 @@
class ConnectivityApiImpl: ConnectivityApi { class ConnectivityApiImpl: ConnectivityApi {
func getCapabilities() throws -> [NetworkCapability] { func getCapabilities(completion: @escaping (Result<[NetworkCapability], any Error>) -> Void) {
[] completion(Result.success([]))
} }
} }

View File

@@ -0,0 +1,9 @@
func dispatch<T>(
qos: DispatchQoS.QoSClass = .default,
completion: @escaping (Result<T, Error>) -> Void,
block: @escaping () throws -> T
) {
DispatchQueue.global(qos: qos).async {
completion(Result { try block() })
}
}

View File

@@ -355,13 +355,13 @@ class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
/// Generated protocol from Pigeon that represents a handler of messages from Flutter. /// Generated protocol from Pigeon that represents a handler of messages from Flutter.
protocol NativeSyncApi { protocol NativeSyncApi {
func shouldFullSync() throws -> Bool func shouldFullSync() throws -> Bool
func getMediaChanges() throws -> SyncDelta func getMediaChanges(completion: @escaping (Result<SyncDelta, Error>) -> Void)
func checkpointSync() throws func checkpointSync() throws
func clearSyncCheckpoint() throws func clearSyncCheckpoint() throws
func getAssetIdsForAlbum(albumId: String) throws -> [String] func getAssetIdsForAlbum(albumId: String, completion: @escaping (Result<[String], Error>) -> Void)
func getAlbums() throws -> [PlatformAlbum] func getAlbums(completion: @escaping (Result<[PlatformAlbum], Error>) -> Void)
func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 func getAssetsCountSince(albumId: String, timestamp: Int64, completion: @escaping (Result<Int64, Error>) -> Void)
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?, completion: @escaping (Result<[PlatformAsset], Error>) -> Void)
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
} }
@@ -395,11 +395,13 @@ class NativeSyncApiSetup {
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
if let api = api { if let api = api {
getMediaChangesChannel.setMessageHandler { _, reply in getMediaChangesChannel.setMessageHandler { _, reply in
do { api.getMediaChanges { result in
let result = try api.getMediaChanges() switch result {
reply(wrapResult(result)) case .success(let res):
} catch { reply(wrapResult(res))
reply(wrapError(error)) case .failure(let error):
reply(wrapError(error))
}
} }
} }
} else { } else {
@@ -438,11 +440,13 @@ class NativeSyncApiSetup {
getAssetIdsForAlbumChannel.setMessageHandler { message, reply in getAssetIdsForAlbumChannel.setMessageHandler { message, reply in
let args = message as! [Any?] let args = message as! [Any?]
let albumIdArg = args[0] as! String let albumIdArg = args[0] as! String
do { api.getAssetIdsForAlbum(albumId: albumIdArg) { result in
let result = try api.getAssetIdsForAlbum(albumId: albumIdArg) switch result {
reply(wrapResult(result)) case .success(let res):
} catch { reply(wrapResult(res))
reply(wrapError(error)) case .failure(let error):
reply(wrapError(error))
}
} }
} }
} else { } else {
@@ -453,11 +457,13 @@ class NativeSyncApiSetup {
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
if let api = api { if let api = api {
getAlbumsChannel.setMessageHandler { _, reply in getAlbumsChannel.setMessageHandler { _, reply in
do { api.getAlbums { result in
let result = try api.getAlbums() switch result {
reply(wrapResult(result)) case .success(let res):
} catch { reply(wrapResult(res))
reply(wrapError(error)) case .failure(let error):
reply(wrapError(error))
}
} }
} }
} else { } else {
@@ -471,11 +477,13 @@ class NativeSyncApiSetup {
let args = message as! [Any?] let args = message as! [Any?]
let albumIdArg = args[0] as! String let albumIdArg = args[0] as! String
let timestampArg = args[1] as! Int64 let timestampArg = args[1] as! Int64
do { api.getAssetsCountSince(albumId: albumIdArg, timestamp: timestampArg) { result in
let result = try api.getAssetsCountSince(albumId: albumIdArg, timestamp: timestampArg) switch result {
reply(wrapResult(result)) case .success(let res):
} catch { reply(wrapResult(res))
reply(wrapError(error)) case .failure(let error):
reply(wrapError(error))
}
} }
} }
} else { } else {
@@ -489,11 +497,13 @@ class NativeSyncApiSetup {
let args = message as! [Any?] let args = message as! [Any?]
let albumIdArg = args[0] as! String let albumIdArg = args[0] as! String
let updatedTimeCondArg: Int64? = nilOrValue(args[1]) let updatedTimeCondArg: Int64? = nilOrValue(args[1])
do { api.getAssetsForAlbum(albumId: albumIdArg, updatedTimeCond: updatedTimeCondArg) { result in
let result = try api.getAssetsForAlbum(albumId: albumIdArg, updatedTimeCond: updatedTimeCondArg) switch result {
reply(wrapResult(result)) case .success(let res):
} catch { reply(wrapResult(res))
reply(wrapError(error)) case .failure(let error):
reply(wrapError(error))
}
} }
} }
} else { } else {

View File

@@ -18,6 +18,7 @@ struct AssetWrapper: Hashable, Equatable {
} }
class NativeSyncApiImpl: NativeSyncApi { class NativeSyncApiImpl: NativeSyncApi {
private let defaults: UserDefaults private let defaults: UserDefaults
private let changeTokenKey = "immich:changeToken" private let changeTokenKey = "immich:changeToken"
private let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum] private let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum]
@@ -75,7 +76,12 @@ class NativeSyncApiImpl: NativeSyncApi {
return false return false
} }
func getAlbums() throws -> [PlatformAlbum] {
func getAlbums(completion: @escaping (Result<[PlatformAlbum], any Error>) -> Void) {
dispatch(qos: .userInitiated, completion: completion, block: getAlbums)
}
private func getAlbums() throws -> [PlatformAlbum] {
var albums: [PlatformAlbum] = [] var albums: [PlatformAlbum] = []
albumTypes.forEach { type in albumTypes.forEach { type in
@@ -112,7 +118,11 @@ class NativeSyncApiImpl: NativeSyncApi {
return albums.sorted { $0.id < $1.id } return albums.sorted { $0.id < $1.id }
} }
func getMediaChanges() throws -> SyncDelta { func getMediaChanges(completion: @escaping (Result<SyncDelta, any Error>) -> Void) {
dispatch(qos: .userInitiated, completion: completion, block: getMediaChanges)
}
private func getMediaChanges() 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)
} }
@@ -198,7 +208,11 @@ class NativeSyncApiImpl: NativeSyncApi {
return albumAssets return albumAssets
} }
func getAssetIdsForAlbum(albumId: String) throws -> [String] { func getAssetIdsForAlbum(albumId: String, completion: @escaping (Result<[String], any Error>) -> Void) {
dispatch(qos: .userInitiated, completion: completion) { try self.getAssetIdsForAlbum(albumId: albumId) }
}
private 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 []
@@ -214,7 +228,13 @@ class NativeSyncApiImpl: NativeSyncApi {
return ids return ids
} }
func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 { func getAssetsCountSince(albumId: String, timestamp: Int64, completion: @escaping (Result<Int64, any Error>) -> Void) {
dispatch(qos: .userInitiated, completion: completion) {
try self.getAssetsCountSince(albumId: albumId, timestamp: timestamp)
}
}
private 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
@@ -228,7 +248,13 @@ class NativeSyncApiImpl: NativeSyncApi {
return Int64(assets.count) return Int64(assets.count)
} }
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] { func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?, completion: @escaping (Result<[PlatformAsset], any Error>) -> Void) {
dispatch(qos: .userInitiated, completion: completion) {
try self.getAssetsForAlbum(albumId: albumId, updatedTimeCond: updatedTimeCond)
}
}
private 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 []

View File

@@ -120,10 +120,6 @@ class RemoteAlbumService {
return _repository.getSharedUsers(albumId); return _repository.getSharedUsers(albumId);
} }
Future<AlbumUserRole?> getUserRole(String albumId, String userId) {
return _repository.getUserRole(albumId, userId);
}
Future<List<RemoteAsset>> getAssets(String albumId) { Future<List<RemoteAsset>> getAssets(String albumId) {
return _repository.getAssets(albumId); return _repository.getAssets(albumId);
} }

View File

@@ -221,15 +221,6 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
.get(); .get();
} }
Future<AlbumUserRole?> getUserRole(String albumId, String userId) async {
final query = _db.remoteAlbumUserEntity.select()
..where((row) => row.albumId.equals(albumId) & row.userId.equals(userId))
..limit(1);
final result = await query.getSingleOrNull();
return result?.role;
}
Future<List<RemoteAsset>> getAssets(String albumId) { Future<List<RemoteAsset>> getAssets(String albumId) {
final query = _db.remoteAlbumAssetEntity.select().join([ final query = _db.remoteAlbumAssetEntity.select().join([
innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)), innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)),

View File

@@ -169,11 +169,9 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
context.pushRoute(const DriftActivitiesRoute()); context.pushRoute(const DriftActivitiesRoute());
} }
Future<void> showOptionSheet(BuildContext context) async { void showOptionSheet(BuildContext context) {
final user = ref.watch(currentUserProvider); final user = ref.watch(currentUserProvider);
final isOwner = user != null ? user.id == _album.ownerId : false; final isOwner = user != null ? user.id == _album.ownerId : false;
final canAddPhotos =
await ref.read(remoteAlbumServiceProvider).getUserRole(_album.id, user?.id ?? '') == AlbumUserRole.editor;
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
@@ -195,30 +193,22 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
context.pop(); context.pop();
} }
: null, : null,
onAddPhotos: isOwner || canAddPhotos onAddPhotos: () async {
? () async { await addAssets(context);
await addAssets(context); context.pop();
context.pop(); },
} onToggleAlbumOrder: () async {
: null, await toggleAlbumOrder();
onToggleAlbumOrder: isOwner context.pop();
? () async { },
await toggleAlbumOrder(); onEditAlbum: () async {
context.pop(); context.pop();
} await showEditTitleAndDescription(context);
: null, },
onEditAlbum: isOwner onCreateSharedLink: () async {
? () async { context.pop();
context.pop(); context.pushRoute(SharedLinkEditRoute(albumId: _album.id));
await showEditTitleAndDescription(context); },
}
: null,
onCreateSharedLink: isOwner
? () async {
context.pop();
context.pushRoute(SharedLinkEditRoute(albumId: _album.id));
}
: null,
onShowOptions: () { onShowOptions: () {
context.pop(); context.pop();
context.pushRoute(const DriftAlbumOptionsRoute()); context.pushRoute(const DriftAlbumOptionsRoute());
@@ -230,9 +220,6 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final user = ref.watch(currentUserProvider);
final isOwner = user != null ? user.id == _album.ownerId : false;
return PopScope( return PopScope(
onPopInvokedWithResult: (didPop, _) { onPopInvokedWithResult: (didPop, _) {
if (didPop) { if (didPop) {
@@ -256,8 +243,8 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
appBar: RemoteAlbumSliverAppBar( appBar: RemoteAlbumSliverAppBar(
icon: Icons.photo_album_outlined, icon: Icons.photo_album_outlined,
onShowOptions: () => showOptionSheet(context), onShowOptions: () => showOptionSheet(context),
onToggleAlbumOrder: isOwner ? () => toggleAlbumOrder() : null, onToggleAlbumOrder: () => toggleAlbumOrder(),
onEditTitle: isOwner ? () => showEditTitleAndDescription(context) : null, onEditTitle: () => showEditTitleAndDescription(context),
onActivity: () => showActivity(context), onActivity: () => showActivity(context),
), ),
bottomSheet: RemoteAlbumBottomSheet(album: _album), bottomSheet: RemoteAlbumBottomSheet(album: _album),

View File

@@ -43,7 +43,7 @@ class ViewerBottomBar extends ConsumerWidget {
final actions = <Widget>[ final actions = <Widget>[
const ShareActionButton(source: ActionSource.viewer), const ShareActionButton(source: ActionSource.viewer),
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer), if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
if (asset.type == AssetType.image && isOwner) const EditImageActionButton(), if (asset.type == AssetType.image) const EditImageActionButton(),
if (isOwner) ...[ if (isOwner) ...[
if (asset.hasRemote && isOwner && isArchived) if (asset.hasRemote && isOwner && isArchived)
const UnArchiveActionButton(source: ActionSource.viewer) const UnArchiveActionButton(source: ActionSource.viewer)

View File

@@ -140,7 +140,6 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull; final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull;
final cameraTitle = _getCameraInfoTitle(exifInfo); final cameraTitle = _getCameraInfoTitle(exifInfo);
final isOwner = ref.watch(currentUserProvider)?.id == (asset is RemoteAsset ? asset.ownerId : null);
return SliverList.list( return SliverList.list(
children: [ children: [
@@ -148,10 +147,10 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
_SheetTile( _SheetTile(
title: _getDateTime(context, asset), title: _getDateTime(context, asset),
titleStyle: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), titleStyle: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
trailing: asset.hasRemote && isOwner ? const Icon(Icons.edit, size: 18) : null, trailing: asset.hasRemote ? const Icon(Icons.edit, size: 18) : null,
onTap: asset.hasRemote && isOwner ? () async => await _editDateTime(context, ref) : null, onTap: asset.hasRemote ? () async => await _editDateTime(context, ref) : null,
), ),
if (exifInfo != null) _SheetAssetDescription(exif: exifInfo, isEditable: isOwner), if (exifInfo != null) _SheetAssetDescription(exif: exifInfo),
const SheetPeopleDetails(), const SheetPeopleDetails(),
const SheetLocationDetails(), const SheetLocationDetails(),
// Details header // Details header
@@ -265,9 +264,8 @@ class _SheetTile extends ConsumerWidget {
class _SheetAssetDescription extends ConsumerStatefulWidget { class _SheetAssetDescription extends ConsumerStatefulWidget {
final ExifInfo exif; final ExifInfo exif;
final bool isEditable;
const _SheetAssetDescription({required this.exif, this.isEditable = true, super.key}); const _SheetAssetDescription({required this.exif});
@override @override
ConsumerState<_SheetAssetDescription> createState() => _SheetAssetDescriptionState(); ConsumerState<_SheetAssetDescription> createState() => _SheetAssetDescriptionState();
@@ -313,33 +311,27 @@ class _SheetAssetDescriptionState extends ConsumerState<_SheetAssetDescription>
// Update controller text when EXIF data changes // Update controller text when EXIF data changes
final currentDescription = currentExifInfo?.description ?? ''; final currentDescription = currentExifInfo?.description ?? '';
final hintText = (widget.isEditable ? 'exif_bottom_sheet_description' : 'exif_bottom_sheet_no_description').t(
context: context,
);
if (_controller.text != currentDescription && !_descriptionFocus.hasFocus) { if (_controller.text != currentDescription && !_descriptionFocus.hasFocus) {
_controller.text = currentDescription; _controller.text = currentDescription;
} }
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
child: IgnorePointer( child: TextField(
ignoring: !widget.isEditable, controller: _controller,
child: TextField( keyboardType: TextInputType.multiline,
controller: _controller, focusNode: _descriptionFocus,
keyboardType: TextInputType.multiline, maxLines: null, // makes it grow as text is added
focusNode: _descriptionFocus, decoration: InputDecoration(
maxLines: null, // makes it grow as text is added hintText: 'exif_bottom_sheet_description'.t(context: context),
decoration: InputDecoration( border: InputBorder.none,
hintText: hintText, enabledBorder: InputBorder.none,
border: InputBorder.none, focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none, disabledBorder: InputBorder.none,
focusedBorder: InputBorder.none, errorBorder: InputBorder.none,
disabledBorder: InputBorder.none, focusedErrorBorder: InputBorder.none,
errorBorder: InputBorder.none,
focusedErrorBorder: InputBorder.none,
),
onTapOutside: (_) => saveDescription(currentExifInfo?.description),
), ),
onTapOutside: (_) => saveDescription(currentExifInfo?.description),
), ),
); );
} }

View File

@@ -44,8 +44,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
final showViewInTimelineButton = final showViewInTimelineButton =
(previousRouteName != TabShellRoute.name || tabRoute == TabEnum.search) && (previousRouteName != TabShellRoute.name || tabRoute == TabEnum.search) &&
previousRouteName != AssetViewerRoute.name && previousRouteName != AssetViewerRoute.name &&
previousRouteName != null && previousRouteName != null;
isOwner;
final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet)); final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet));
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity)); int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));

View File

@@ -24,7 +24,6 @@ import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_shee
import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart';
class RemoteAlbumBottomSheet extends ConsumerStatefulWidget { class RemoteAlbumBottomSheet extends ConsumerStatefulWidget {
@@ -54,7 +53,6 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
Widget build(BuildContext context) { Widget build(BuildContext context) {
final multiselect = ref.watch(multiSelectProvider); final multiselect = ref.watch(multiSelectProvider);
final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
final ownsAlbum = ref.watch(currentUserProvider)?.id == widget.album.ownerId;
Future<void> addAssetsToAlbum(RemoteAlbum album) async { Future<void> addAssetsToAlbum(RemoteAlbum album) async {
final selectedAssets = multiselect.selectedAssets; final selectedAssets = multiselect.selectedAssets;
@@ -95,35 +93,28 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
const ShareActionButton(source: ActionSource.timeline), const ShareActionButton(source: ActionSource.timeline),
if (multiselect.hasRemote) ...[ if (multiselect.hasRemote) ...[
const ShareLinkActionButton(source: ActionSource.timeline), const ShareLinkActionButton(source: ActionSource.timeline),
const ArchiveActionButton(source: ActionSource.timeline),
if (ownsAlbum) ...[ const FavoriteActionButton(source: ActionSource.timeline),
const ArchiveActionButton(source: ActionSource.timeline),
const FavoriteActionButton(source: ActionSource.timeline),
],
const DownloadActionButton(source: ActionSource.timeline), const DownloadActionButton(source: ActionSource.timeline),
if (ownsAlbum) ...[ isTrashEnable
isTrashEnable ? const TrashActionButton(source: ActionSource.timeline)
? const TrashActionButton(source: ActionSource.timeline) : const DeletePermanentActionButton(source: ActionSource.timeline),
: const DeletePermanentActionButton(source: ActionSource.timeline), const EditDateTimeActionButton(source: ActionSource.timeline),
const EditDateTimeActionButton(source: ActionSource.timeline), const EditLocationActionButton(source: ActionSource.timeline),
const EditLocationActionButton(source: ActionSource.timeline), const MoveToLockFolderActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(source: ActionSource.timeline), if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline), if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
],
], ],
if (multiselect.hasLocal) ...[ if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline), const DeleteLocalActionButton(source: ActionSource.timeline),
const UploadActionButton(source: ActionSource.timeline), const UploadActionButton(source: ActionSource.timeline),
], ],
if (ownsAlbum) RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id), RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id),
],
slivers: [
const AddToAlbumHeader(),
AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand),
], ],
slivers: ownsAlbum
? [
const AddToAlbumHeader(),
AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand),
]
: null,
); );
} }
} }

View File

@@ -20,10 +20,13 @@ class BackgroundWorkerSettings {
@HostApi() @HostApi()
abstract class BackgroundWorkerFgHostApi { abstract class BackgroundWorkerFgHostApi {
@async
void enable(); void enable();
@async
void configure(BackgroundWorkerSettings settings); void configure(BackgroundWorkerSettings settings);
@async
void disable(); void disable();
} }

View File

@@ -11,7 +11,9 @@ import 'package:pigeon/pigeon.dart';
) )
@HostApi() @HostApi()
abstract class BackgroundWorkerLockApi { abstract class BackgroundWorkerLockApi {
@async
void lock(); void lock();
@async
void unlock(); void unlock();
} }

View File

@@ -15,6 +15,7 @@ enum NetworkCapability { cellular, wifi, vpn, unmetered }
@HostApi() @HostApi()
abstract class ConnectivityApi { abstract class ConnectivityApi {
@async
@TaskQueue(type: TaskQueueType.serialBackgroundThread) @TaskQueue(type: TaskQueueType.serialBackgroundThread)
List<NetworkCapability> getCapabilities(); List<NetworkCapability> getCapabilities();
} }

View File

@@ -83,6 +83,7 @@ class HashResult {
abstract class NativeSyncApi { abstract class NativeSyncApi {
bool shouldFullSync(); bool shouldFullSync();
@async
@TaskQueue(type: TaskQueueType.serialBackgroundThread) @TaskQueue(type: TaskQueueType.serialBackgroundThread)
SyncDelta getMediaChanges(); SyncDelta getMediaChanges();
@@ -90,15 +91,19 @@ abstract class NativeSyncApi {
void clearSyncCheckpoint(); void clearSyncCheckpoint();
@async
@TaskQueue(type: TaskQueueType.serialBackgroundThread) @TaskQueue(type: TaskQueueType.serialBackgroundThread)
List<String> getAssetIdsForAlbum(String albumId); List<String> getAssetIdsForAlbum(String albumId);
@async
@TaskQueue(type: TaskQueueType.serialBackgroundThread) @TaskQueue(type: TaskQueueType.serialBackgroundThread)
List<PlatformAlbum> getAlbums(); List<PlatformAlbum> getAlbums();
@async
@TaskQueue(type: TaskQueueType.serialBackgroundThread) @TaskQueue(type: TaskQueueType.serialBackgroundThread)
int getAssetsCountSince(String albumId, int timestamp); int getAssetsCountSince(String albumId, int timestamp);
@async
@TaskQueue(type: TaskQueueType.serialBackgroundThread) @TaskQueue(type: TaskQueueType.serialBackgroundThread)
List<PlatformAsset> getAssetsForAlbum(String albumId, {int? updatedTimeCond}); List<PlatformAsset> getAssetsForAlbum(String albumId, {int? updatedTimeCond});