Compare commits
1 Commits
fix/async-
...
refactor/t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9051fa6949 |
@@ -3,6 +3,7 @@ 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() {
|
||||||
@@ -16,5 +17,6 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
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() })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,50 +2,49 @@ 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(callback: (Result<Unit>) -> Unit) = dispatch(callback = callback) {
|
override fun lock() {
|
||||||
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(callback: (Result<Unit>) -> Unit) = dispatch(callback = callback) {
|
override fun unlock() {
|
||||||
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,12 +133,11 @@ 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(callback: (Result<Unit>) -> Unit)
|
fun enable()
|
||||||
fun configure(settings: BackgroundWorkerSettings, callback: (Result<Unit>) -> Unit)
|
fun configure(settings: BackgroundWorkerSettings)
|
||||||
fun disable(callback: (Result<Unit>) -> Unit)
|
fun disable()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** The codec used by BackgroundWorkerFgHostApi. */
|
/** The codec used by BackgroundWorkerFgHostApi. */
|
||||||
@@ -153,14 +152,13 @@ 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 ->
|
||||||
api.enable{ result: Result<Unit> ->
|
val wrapped: List<Any?> = try {
|
||||||
val error = result.exceptionOrNull()
|
api.enable()
|
||||||
if (error != null) {
|
listOf(null)
|
||||||
reply.reply(BackgroundWorkerPigeonUtils.wrapError(error))
|
} catch (exception: Throwable) {
|
||||||
} else {
|
BackgroundWorkerPigeonUtils.wrapError(exception)
|
||||||
reply.reply(BackgroundWorkerPigeonUtils.wrapResult(null))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
@@ -172,14 +170,13 @@ 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
|
||||||
api.configure(settingsArg) { result: Result<Unit> ->
|
val wrapped: List<Any?> = try {
|
||||||
val error = result.exceptionOrNull()
|
api.configure(settingsArg)
|
||||||
if (error != null) {
|
listOf(null)
|
||||||
reply.reply(BackgroundWorkerPigeonUtils.wrapError(error))
|
} catch (exception: Throwable) {
|
||||||
} else {
|
BackgroundWorkerPigeonUtils.wrapError(exception)
|
||||||
reply.reply(BackgroundWorkerPigeonUtils.wrapResult(null))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
@@ -189,14 +186,13 @@ 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 ->
|
||||||
api.disable{ result: Result<Unit> ->
|
val wrapped: List<Any?> = try {
|
||||||
val error = result.exceptionOrNull()
|
api.disable()
|
||||||
if (error != null) {
|
listOf(null)
|
||||||
reply.reply(BackgroundWorkerPigeonUtils.wrapError(error))
|
} catch (exception: Throwable) {
|
||||||
} else {
|
BackgroundWorkerPigeonUtils.wrapError(exception)
|
||||||
reply.reply(BackgroundWorkerPigeonUtils.wrapResult(null))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
|
|||||||
@@ -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 (_: Exception) {
|
} catch (e: Exception) {
|
||||||
// ignored, there is nothing to be done
|
// ignored, there is nothing to be done
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ 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
|
||||||
|
|
||||||
@@ -17,18 +16,16 @@ 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(callback: (Result<Unit>) -> Unit) =
|
override fun enable() {
|
||||||
dispatch(callback = callback) { enqueueMediaObserver(ctx) }
|
enqueueMediaObserver(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
override fun configure(
|
override fun configure(settings: BackgroundWorkerSettings) {
|
||||||
settings: BackgroundWorkerSettings,
|
|
||||||
callback: (Result<Unit>) -> Unit
|
|
||||||
) = dispatch(callback = callback) {
|
|
||||||
BackgroundWorkerPreferences(ctx).updateSettings(settings)
|
BackgroundWorkerPreferences(ctx).updateSettings(settings)
|
||||||
enqueueMediaObserver(ctx)
|
enqueueMediaObserver(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun disable(callback: (Result<Unit>) -> Unit) = dispatch(callback = callback) {
|
override fun disable() {
|
||||||
WorkManager.getInstance(ctx).apply {
|
WorkManager.getInstance(ctx).apply {
|
||||||
cancelUniqueWork(OBSERVER_WORKER_NAME)
|
cancelUniqueWork(OBSERVER_WORKER_NAME)
|
||||||
cancelUniqueWork(BACKGROUND_WORKER_NAME)
|
cancelUniqueWork(BACKGROUND_WORKER_NAME)
|
||||||
@@ -41,6 +38,7 @@ 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 {
|
||||||
|
|||||||
@@ -44,11 +44,10 @@ 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(callback: (Result<Unit>) -> Unit)
|
fun lock()
|
||||||
fun unlock(callback: (Result<Unit>) -> Unit)
|
fun unlock()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** The codec used by BackgroundWorkerLockApi. */
|
/** The codec used by BackgroundWorkerLockApi. */
|
||||||
@@ -63,14 +62,13 @@ 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 ->
|
||||||
api.lock{ result: Result<Unit> ->
|
val wrapped: List<Any?> = try {
|
||||||
val error = result.exceptionOrNull()
|
api.lock()
|
||||||
if (error != null) {
|
listOf(null)
|
||||||
reply.reply(BackgroundWorkerLockPigeonUtils.wrapError(error))
|
} catch (exception: Throwable) {
|
||||||
} else {
|
BackgroundWorkerLockPigeonUtils.wrapError(exception)
|
||||||
reply.reply(BackgroundWorkerLockPigeonUtils.wrapResult(null))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
@@ -80,14 +78,13 @@ 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 ->
|
||||||
api.unlock{ result: Result<Unit> ->
|
val wrapped: List<Any?> = try {
|
||||||
val error = result.exceptionOrNull()
|
api.unlock()
|
||||||
if (error != null) {
|
listOf(null)
|
||||||
reply.reply(BackgroundWorkerLockPigeonUtils.wrapError(error))
|
} catch (exception: Throwable) {
|
||||||
} else {
|
BackgroundWorkerLockPigeonUtils.wrapError(exception)
|
||||||
reply.reply(BackgroundWorkerLockPigeonUtils.wrapResult(null))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
|
|||||||
@@ -82,10 +82,9 @@ 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(callback: (Result<List<NetworkCapability>>) -> Unit)
|
fun getCapabilities(): List<NetworkCapability>
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** The codec used by ConnectivityApi. */
|
/** The codec used by ConnectivityApi. */
|
||||||
@@ -101,15 +100,12 @@ 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 ->
|
||||||
api.getCapabilities{ result: Result<List<NetworkCapability>> ->
|
val wrapped: List<Any?> = try {
|
||||||
val error = result.exceptionOrNull()
|
listOf(api.getCapabilities())
|
||||||
if (error != null) {
|
} catch (exception: Throwable) {
|
||||||
reply.reply(ConnectivityPigeonUtils.wrapError(error))
|
ConnectivityPigeonUtils.wrapError(exception)
|
||||||
} else {
|
|
||||||
val data = result.getOrNull()
|
|
||||||
reply.reply(ConnectivityPigeonUtils.wrapResult(data))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ 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 =
|
||||||
@@ -12,13 +11,7 @@ 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()
|
||||||
|
|
||||||
|
|||||||
@@ -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(callback: (Result<SyncDelta>) -> Unit)
|
fun getMediaChanges(): SyncDelta
|
||||||
fun checkpointSync()
|
fun checkpointSync()
|
||||||
fun clearSyncCheckpoint()
|
fun clearSyncCheckpoint()
|
||||||
fun getAssetIdsForAlbum(albumId: String, callback: (Result<List<String>>) -> Unit)
|
fun getAssetIdsForAlbum(albumId: String): List<String>
|
||||||
fun getAlbums(callback: (Result<List<PlatformAlbum>>) -> Unit)
|
fun getAlbums(): List<PlatformAlbum>
|
||||||
fun getAssetsCountSince(albumId: String, timestamp: Long, callback: (Result<Long>) -> Unit)
|
fun getAssetsCountSince(albumId: String, timestamp: Long): Long
|
||||||
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?, callback: (Result<List<PlatformAsset>>) -> Unit)
|
fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<PlatformAsset>
|
||||||
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,15 +335,12 @@ 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 ->
|
||||||
api.getMediaChanges{ result: Result<SyncDelta> ->
|
val wrapped: List<Any?> = try {
|
||||||
val error = result.exceptionOrNull()
|
listOf(api.getMediaChanges())
|
||||||
if (error != null) {
|
} catch (exception: Throwable) {
|
||||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
MessagesPigeonUtils.wrapError(exception)
|
||||||
} else {
|
|
||||||
val data = result.getOrNull()
|
|
||||||
reply.reply(MessagesPigeonUtils.wrapResult(data))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
@@ -387,15 +384,12 @@ 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
|
||||||
api.getAssetIdsForAlbum(albumIdArg) { result: Result<List<String>> ->
|
val wrapped: List<Any?> = try {
|
||||||
val error = result.exceptionOrNull()
|
listOf(api.getAssetIdsForAlbum(albumIdArg))
|
||||||
if (error != null) {
|
} catch (exception: Throwable) {
|
||||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
MessagesPigeonUtils.wrapError(exception)
|
||||||
} else {
|
|
||||||
val data = result.getOrNull()
|
|
||||||
reply.reply(MessagesPigeonUtils.wrapResult(data))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
@@ -405,15 +399,12 @@ 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 ->
|
||||||
api.getAlbums{ result: Result<List<PlatformAlbum>> ->
|
val wrapped: List<Any?> = try {
|
||||||
val error = result.exceptionOrNull()
|
listOf(api.getAlbums())
|
||||||
if (error != null) {
|
} catch (exception: Throwable) {
|
||||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
MessagesPigeonUtils.wrapError(exception)
|
||||||
} else {
|
|
||||||
val data = result.getOrNull()
|
|
||||||
reply.reply(MessagesPigeonUtils.wrapResult(data))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
@@ -426,15 +417,12 @@ 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
|
||||||
api.getAssetsCountSince(albumIdArg, timestampArg) { result: Result<Long> ->
|
val wrapped: List<Any?> = try {
|
||||||
val error = result.exceptionOrNull()
|
listOf(api.getAssetsCountSince(albumIdArg, timestampArg))
|
||||||
if (error != null) {
|
} catch (exception: Throwable) {
|
||||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
MessagesPigeonUtils.wrapError(exception)
|
||||||
} else {
|
|
||||||
val data = result.getOrNull()
|
|
||||||
reply.reply(MessagesPigeonUtils.wrapResult(data))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
@@ -447,15 +435,12 @@ 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?
|
||||||
api.getAssetsForAlbum(albumIdArg, updatedTimeCondArg) { result: Result<List<PlatformAsset>> ->
|
val wrapped: List<Any?> = try {
|
||||||
val error = result.exceptionOrNull()
|
listOf(api.getAssetsForAlbum(albumIdArg, updatedTimeCondArg))
|
||||||
if (error != null) {
|
} catch (exception: Throwable) {
|
||||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
MessagesPigeonUtils.wrapError(exception)
|
||||||
} else {
|
|
||||||
val data = result.getOrNull()
|
|
||||||
reply.reply(MessagesPigeonUtils.wrapResult(data))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
|
|||||||
@@ -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(callback: (Result<SyncDelta>) -> Unit) =
|
override fun getMediaChanges(): SyncDelta {
|
||||||
callback(Result.failure(IllegalStateException("Method not supported on this Android version.")))
|
throw IllegalStateException("Method not supported on this Android version.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ 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)
|
||||||
@@ -48,12 +47,7 @@ class NativeSyncApiImpl30(context: Context) : NativeSyncApiImplBase(context), Na
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMediaChanges(callback: (Result<SyncDelta>) -> Unit) =
|
override fun getMediaChanges(): SyncDelta {
|
||||||
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>()
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ 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
|
||||||
@@ -146,10 +145,7 @@ open class NativeSyncApiImplBase(context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAlbums(callback: (Result<List<PlatformAlbum>>) -> Unit) =
|
fun getAlbums(): List<PlatformAlbum> {
|
||||||
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>()
|
||||||
|
|
||||||
@@ -196,10 +192,7 @@ open class NativeSyncApiImplBase(context: Context) {
|
|||||||
.sortedBy { it.id }
|
.sortedBy { it.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAssetIdsForAlbum(albumId: String, callback: (Result<List<String>>) -> Unit) =
|
fun getAssetIdsForAlbum(albumId: String): List<String> {
|
||||||
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(
|
||||||
@@ -215,23 +208,15 @@ open class NativeSyncApiImplBase(context: Context) {
|
|||||||
} ?: emptyList()
|
} ?: emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAssetsCountSince(albumId: String, timestamp: Long, callback: (Result<Long>) -> Unit) =
|
fun getAssetsCountSince(albumId: String, timestamp: Long): Long =
|
||||||
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) }
|
|
||||||
|
|
||||||
private fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?): List<PlatformAsset> {
|
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)
|
||||||
|
|
||||||
@@ -269,7 +254,7 @@ open class NativeSyncApiImplBase(context: Context) {
|
|||||||
}.awaitAll()
|
}.awaitAll()
|
||||||
|
|
||||||
callback(Result.success(results))
|
callback(Result.success(results))
|
||||||
} catch (_: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
callback(
|
callback(
|
||||||
Result.failure(
|
Result.failure(
|
||||||
FlutterError(
|
FlutterError(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 77;
|
objectVersion = 54;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
@@ -133,14 +133,11 @@
|
|||||||
/* 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 = (
|
||||||
@@ -250,7 +247,6 @@
|
|||||||
97C146F01CF9000F007C117D /* Runner */ = {
|
97C146F01CF9000F007C117D /* Runner */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
B2D27ABE2E84A0FF004DD55B /* Core */,
|
|
||||||
B25D37792E72CA15008B6CA7 /* Connectivity */,
|
B25D37792E72CA15008B6CA7 /* Connectivity */,
|
||||||
B21E34A62E5AF9760031FDB9 /* Background */,
|
B21E34A62E5AF9760031FDB9 /* Background */,
|
||||||
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
|
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
|
||||||
@@ -336,7 +332,6 @@
|
|||||||
);
|
);
|
||||||
fileSystemSynchronizedGroups = (
|
fileSystemSynchronizedGroups = (
|
||||||
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
|
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
|
||||||
B2D27ABE2E84A0FF004DD55B /* Core */,
|
|
||||||
);
|
);
|
||||||
name = Runner;
|
name = Runner;
|
||||||
productName = Runner;
|
productName = Runner;
|
||||||
@@ -526,14 +521,10 @@
|
|||||||
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";
|
||||||
@@ -562,14 +553,10 @@
|
|||||||
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";
|
||||||
|
|||||||
@@ -179,12 +179,11 @@ 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(completion: @escaping (Result<Void, Error>) -> Void)
|
func enable() throws
|
||||||
func configure(settings: BackgroundWorkerSettings, completion: @escaping (Result<Void, Error>) -> Void)
|
func configure(settings: BackgroundWorkerSettings) throws
|
||||||
func disable(completion: @escaping (Result<Void, Error>) -> Void)
|
func disable() throws
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||||
@@ -196,13 +195,11 @@ 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
|
||||||
api.enable { result in
|
do {
|
||||||
switch result {
|
try api.enable()
|
||||||
case .success:
|
reply(wrapResult(nil))
|
||||||
reply(wrapResult(nil))
|
} catch {
|
||||||
case .failure(let error):
|
reply(wrapError(error))
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -213,13 +210,11 @@ 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
|
||||||
api.configure(settings: settingsArg) { result in
|
do {
|
||||||
switch result {
|
try api.configure(settings: settingsArg)
|
||||||
case .success:
|
reply(wrapResult(nil))
|
||||||
reply(wrapResult(nil))
|
} catch {
|
||||||
case .failure(let error):
|
reply(wrapError(error))
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -228,13 +223,11 @@ 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
|
||||||
api.disable { result in
|
do {
|
||||||
switch result {
|
try api.disable()
|
||||||
case .success:
|
reply(wrapResult(nil))
|
||||||
reply(wrapResult(nil))
|
} catch {
|
||||||
case .failure(let error):
|
reply(wrapError(error))
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,27 +1,23 @@
|
|||||||
import BackgroundTasks
|
import BackgroundTasks
|
||||||
|
|
||||||
class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi {
|
||||||
func enable(completion: @escaping (Result<Void, any Error>) -> Void) {
|
|
||||||
dispatch(completion: completion) {
|
|
||||||
BackgroundWorkerApiImpl.scheduleRefreshWorker()
|
|
||||||
BackgroundWorkerApiImpl.scheduleProcessingWorker()
|
|
||||||
print("BackgroundWorkerApiImpl:enable Background worker scheduled")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func configure(settings: BackgroundWorkerSettings, completion: @escaping (Result<Void, any Error>) -> Void) {
|
|
||||||
// Android only
|
|
||||||
completion(Result.success(Void()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func disable(completion: @escaping (Result<Void, any Error>) -> Void) {
|
|
||||||
dispatch(completion: completion) {
|
|
||||||
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.refreshTaskID);
|
|
||||||
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.processingTaskID);
|
|
||||||
print("BackgroundWorkerApiImpl:disableUploadWorker Disabled background workers")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
func enable() throws {
|
||||||
|
BackgroundWorkerApiImpl.scheduleRefreshWorker()
|
||||||
|
BackgroundWorkerApiImpl.scheduleProcessingWorker()
|
||||||
|
print("BackgroundWorkerApiImpl:enable Background worker scheduled")
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure(settings: BackgroundWorkerSettings) throws {
|
||||||
|
// Android only
|
||||||
|
}
|
||||||
|
|
||||||
|
func disable() throws {
|
||||||
|
BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.refreshTaskID);
|
||||||
|
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"
|
||||||
private static let processingTaskID = "app.alextran.immich.background.processingUpload"
|
private static let processingTaskID = "app.alextran.immich.background.processingUpload"
|
||||||
private static let taskSemaphore = DispatchSemaphore(value: 1)
|
private static let taskSemaphore = DispatchSemaphore(value: 1)
|
||||||
|
|||||||
@@ -94,10 +94,9 @@ 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(completion: @escaping (Result<[NetworkCapability], Error>) -> Void)
|
func getCapabilities() throws -> [NetworkCapability]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||||
@@ -116,13 +115,11 @@ 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
|
||||||
api.getCapabilities { result in
|
do {
|
||||||
switch result {
|
let result = try api.getCapabilities()
|
||||||
case .success(let res):
|
reply(wrapResult(result))
|
||||||
reply(wrapResult(res))
|
} catch {
|
||||||
case .failure(let error):
|
reply(wrapError(error))
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
class ConnectivityApiImpl: ConnectivityApi {
|
class ConnectivityApiImpl: ConnectivityApi {
|
||||||
func getCapabilities(completion: @escaping (Result<[NetworkCapability], any Error>) -> Void) {
|
func getCapabilities() throws -> [NetworkCapability] {
|
||||||
completion(Result.success([]))
|
[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
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() })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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(completion: @escaping (Result<SyncDelta, Error>) -> Void)
|
func getMediaChanges() throws -> SyncDelta
|
||||||
func checkpointSync() throws
|
func checkpointSync() throws
|
||||||
func clearSyncCheckpoint() throws
|
func clearSyncCheckpoint() throws
|
||||||
func getAssetIdsForAlbum(albumId: String, completion: @escaping (Result<[String], Error>) -> Void)
|
func getAssetIdsForAlbum(albumId: String) throws -> [String]
|
||||||
func getAlbums(completion: @escaping (Result<[PlatformAlbum], Error>) -> Void)
|
func getAlbums() throws -> [PlatformAlbum]
|
||||||
func getAssetsCountSince(albumId: String, timestamp: Int64, completion: @escaping (Result<Int64, Error>) -> Void)
|
func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64
|
||||||
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?, completion: @escaping (Result<[PlatformAsset], Error>) -> Void)
|
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset]
|
||||||
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,13 +395,11 @@ 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
|
||||||
api.getMediaChanges { result in
|
do {
|
||||||
switch result {
|
let result = try api.getMediaChanges()
|
||||||
case .success(let res):
|
reply(wrapResult(result))
|
||||||
reply(wrapResult(res))
|
} catch {
|
||||||
case .failure(let error):
|
reply(wrapError(error))
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -440,13 +438,11 @@ 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
|
||||||
api.getAssetIdsForAlbum(albumId: albumIdArg) { result in
|
do {
|
||||||
switch result {
|
let result = try api.getAssetIdsForAlbum(albumId: albumIdArg)
|
||||||
case .success(let res):
|
reply(wrapResult(result))
|
||||||
reply(wrapResult(res))
|
} catch {
|
||||||
case .failure(let error):
|
reply(wrapError(error))
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -457,13 +453,11 @@ 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
|
||||||
api.getAlbums { result in
|
do {
|
||||||
switch result {
|
let result = try api.getAlbums()
|
||||||
case .success(let res):
|
reply(wrapResult(result))
|
||||||
reply(wrapResult(res))
|
} catch {
|
||||||
case .failure(let error):
|
reply(wrapError(error))
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -477,13 +471,11 @@ 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
|
||||||
api.getAssetsCountSince(albumId: albumIdArg, timestamp: timestampArg) { result in
|
do {
|
||||||
switch result {
|
let result = try api.getAssetsCountSince(albumId: albumIdArg, timestamp: timestampArg)
|
||||||
case .success(let res):
|
reply(wrapResult(result))
|
||||||
reply(wrapResult(res))
|
} catch {
|
||||||
case .failure(let error):
|
reply(wrapError(error))
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -497,13 +489,11 @@ 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])
|
||||||
api.getAssetsForAlbum(albumId: albumIdArg, updatedTimeCond: updatedTimeCondArg) { result in
|
do {
|
||||||
switch result {
|
let result = try api.getAssetsForAlbum(albumId: albumIdArg, updatedTimeCond: updatedTimeCondArg)
|
||||||
case .success(let res):
|
reply(wrapResult(result))
|
||||||
reply(wrapResult(res))
|
} catch {
|
||||||
case .failure(let error):
|
reply(wrapError(error))
|
||||||
reply(wrapError(error))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ 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]
|
||||||
@@ -76,12 +75,7 @@ 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
|
||||||
@@ -118,11 +112,7 @@ class NativeSyncApiImpl: NativeSyncApi {
|
|||||||
return albums.sorted { $0.id < $1.id }
|
return albums.sorted { $0.id < $1.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMediaChanges(completion: @escaping (Result<SyncDelta, any Error>) -> Void) {
|
func getMediaChanges() throws -> SyncDelta {
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -208,11 +198,7 @@ class NativeSyncApiImpl: NativeSyncApi {
|
|||||||
return albumAssets
|
return albumAssets
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAssetIdsForAlbum(albumId: String, completion: @escaping (Result<[String], any Error>) -> Void) {
|
func getAssetIdsForAlbum(albumId: String) throws -> [String] {
|
||||||
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 []
|
||||||
@@ -228,13 +214,7 @@ class NativeSyncApiImpl: NativeSyncApi {
|
|||||||
return ids
|
return ids
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAssetsCountSince(albumId: String, timestamp: Int64, completion: @escaping (Result<Int64, any Error>) -> Void) {
|
func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 {
|
||||||
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
|
||||||
@@ -248,13 +228,7 @@ class NativeSyncApiImpl: NativeSyncApi {
|
|||||||
return Int64(assets.count)
|
return Int64(assets.count)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?, completion: @escaping (Result<[PlatformAsset], any Error>) -> Void) {
|
func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?) throws -> [PlatformAsset] {
|
||||||
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 []
|
||||||
|
|||||||
@@ -20,13 +20,10 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ import 'package:pigeon/pigeon.dart';
|
|||||||
)
|
)
|
||||||
@HostApi()
|
@HostApi()
|
||||||
abstract class BackgroundWorkerLockApi {
|
abstract class BackgroundWorkerLockApi {
|
||||||
@async
|
|
||||||
void lock();
|
void lock();
|
||||||
|
|
||||||
@async
|
|
||||||
void unlock();
|
void unlock();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ 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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ 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();
|
||||||
|
|
||||||
@@ -91,19 +90,15 @@ 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});
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import type { ScrubberMonth } from '$lib/managers/timeline-manager/types';
|
import type { ScrubberMonth } from '$lib/managers/timeline-manager/types';
|
||||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||||
import { getTabbable } from '$lib/utils/focus-util';
|
import { getTabbable } from '$lib/utils/focus-util';
|
||||||
import { type ScrubberListener } from '$lib/utils/timeline-util';
|
import { type ScrubberListener, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||||
import { Icon } from '@immich/ui';
|
import { Icon } from '@immich/ui';
|
||||||
import { mdiPlay } from '@mdi/js';
|
import { mdiPlay } from '@mdi/js';
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from 'lodash-es';
|
||||||
@@ -11,18 +11,31 @@
|
|||||||
import { fade, fly } from 'svelte/transition';
|
import { fade, fly } from 'svelte/transition';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
/** Offset from the top of the timeline (e.g., for headers) */
|
||||||
timelineTopOffset?: number;
|
timelineTopOffset?: number;
|
||||||
|
/** Offset from the bottom of the timeline (e.g., for footers) */
|
||||||
timelineBottomOffset?: number;
|
timelineBottomOffset?: number;
|
||||||
|
/** Total height of the scrubber component */
|
||||||
height?: number;
|
height?: number;
|
||||||
|
/** Timeline manager instance that controls the timeline state */
|
||||||
timelineManager: TimelineManager;
|
timelineManager: TimelineManager;
|
||||||
scrubOverallPercent?: number;
|
/** Overall scroll percentage through the entire timeline (0-1), used when no specific month is targeted */
|
||||||
scrubberMonthPercent?: number;
|
timelineScrollPercent?: number;
|
||||||
scrubberMonth?: { year: number; month: number };
|
/** The percentage of scroll through the month that is currently intersecting the top boundary of the viewport */
|
||||||
leadout?: boolean;
|
viewportTopMonthScrollPercent?: number;
|
||||||
|
/** The year/month of the timeline month at the top of the viewport */
|
||||||
|
viewportTopMonth?: TimelineYearMonth;
|
||||||
|
/** Indicates whether the viewport is currently in the lead-out section (after all months) */
|
||||||
|
isInLeadOutSection?: boolean;
|
||||||
|
/** Width of the scrubber component in pixels (bindable for parent component margin adjustments) */
|
||||||
scrubberWidth?: number;
|
scrubberWidth?: number;
|
||||||
|
/** Callback fired when user interacts with the scrubber to navigate */
|
||||||
onScrub?: ScrubberListener;
|
onScrub?: ScrubberListener;
|
||||||
|
/** Callback fired when keyboard events occur on the scrubber */
|
||||||
onScrubKeyDown?: (event: KeyboardEvent, element: HTMLElement) => void;
|
onScrubKeyDown?: (event: KeyboardEvent, element: HTMLElement) => void;
|
||||||
|
/** Callback fired when scrubbing starts */
|
||||||
startScrub?: ScrubberListener;
|
startScrub?: ScrubberListener;
|
||||||
|
/** Callback fired when scrubbing stops */
|
||||||
stopScrub?: ScrubberListener;
|
stopScrub?: ScrubberListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,10 +44,10 @@
|
|||||||
timelineBottomOffset = 0,
|
timelineBottomOffset = 0,
|
||||||
height = 0,
|
height = 0,
|
||||||
timelineManager,
|
timelineManager,
|
||||||
scrubOverallPercent = 0,
|
timelineScrollPercent = 0,
|
||||||
scrubberMonthPercent = 0,
|
viewportTopMonthScrollPercent = 0,
|
||||||
scrubberMonth = undefined,
|
viewportTopMonth = undefined,
|
||||||
leadout = false,
|
isInLeadOutSection = false,
|
||||||
onScrub = undefined,
|
onScrub = undefined,
|
||||||
onScrubKeyDown = undefined,
|
onScrubKeyDown = undefined,
|
||||||
startScrub = undefined,
|
startScrub = undefined,
|
||||||
@@ -100,7 +113,7 @@
|
|||||||
offset += scrubberMonthPercent * relativeBottomOffset;
|
offset += scrubberMonthPercent * relativeBottomOffset;
|
||||||
}
|
}
|
||||||
return offset;
|
return offset;
|
||||||
} else if (leadout) {
|
} else if (isInLeadOutSection) {
|
||||||
let offset = relativeTopOffset;
|
let offset = relativeTopOffset;
|
||||||
for (const segment of segments) {
|
for (const segment of segments) {
|
||||||
offset += segment.height;
|
offset += segment.height;
|
||||||
@@ -111,7 +124,9 @@
|
|||||||
return scrubOverallPercent * (height - (PADDING_TOP + PADDING_BOTTOM));
|
return scrubOverallPercent * (height - (PADDING_TOP + PADDING_BOTTOM));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let scrollY = $derived(toScrollFromMonthGroupPercentage(scrubberMonth, scrubberMonthPercent, scrubOverallPercent));
|
let scrollY = $derived(
|
||||||
|
toScrollFromMonthGroupPercentage(viewportTopMonth, viewportTopMonthScrollPercent, timelineScrollPercent),
|
||||||
|
);
|
||||||
let timelineFullHeight = $derived(timelineManager.scrubberTimelineHeight + timelineTopOffset + timelineBottomOffset);
|
let timelineFullHeight = $derived(timelineManager.scrubberTimelineHeight + timelineTopOffset + timelineBottomOffset);
|
||||||
let relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight));
|
let relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight));
|
||||||
let relativeBottomOffset = $derived(toScrollY(timelineBottomOffset / timelineFullHeight));
|
let relativeBottomOffset = $derived(toScrollY(timelineBottomOffset / timelineFullHeight));
|
||||||
@@ -295,12 +310,24 @@
|
|||||||
|
|
||||||
const scrollPercent = toTimelineY(hoverY);
|
const scrollPercent = toTimelineY(hoverY);
|
||||||
if (wasDragging === false && isDragging) {
|
if (wasDragging === false && isDragging) {
|
||||||
void startScrub?.(segmentDate!, scrollPercent, monthGroupPercentY);
|
void startScrub?.({
|
||||||
void onScrub?.(segmentDate!, scrollPercent, monthGroupPercentY);
|
scrubberMonth: segmentDate!,
|
||||||
|
overallScrollPercent: scrollPercent,
|
||||||
|
scrubberMonthScrollPercent: monthGroupPercentY,
|
||||||
|
});
|
||||||
|
void onScrub?.({
|
||||||
|
scrubberMonth: segmentDate!,
|
||||||
|
overallScrollPercent: scrollPercent,
|
||||||
|
scrubberMonthScrollPercent: monthGroupPercentY,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wasDragging && !isDragging) {
|
if (wasDragging && !isDragging) {
|
||||||
void stopScrub?.(segmentDate!, scrollPercent, monthGroupPercentY);
|
void stopScrub?.({
|
||||||
|
scrubberMonth: segmentDate!,
|
||||||
|
overallScrollPercent: scrollPercent,
|
||||||
|
scrubberMonthScrollPercent: monthGroupPercentY,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +335,11 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void onScrub?.(segmentDate!, scrollPercent, monthGroupPercentY);
|
void onScrub?.({
|
||||||
|
scrubberMonth: segmentDate!,
|
||||||
|
overallScrollPercent: scrollPercent,
|
||||||
|
scrubberMonthScrollPercent: monthGroupPercentY,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
/* eslint-disable tscompat/tscompat */
|
/* eslint-disable tscompat/tscompat */
|
||||||
const getTouch = (event: TouchEvent) => {
|
const getTouch = (event: TouchEvent) => {
|
||||||
@@ -412,7 +443,11 @@
|
|||||||
}
|
}
|
||||||
if (next) {
|
if (next) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
void onScrub?.({ year: next.year, month: next.month }, -1, 0);
|
void onScrub?.({
|
||||||
|
scrubberMonth: { year: next.year, month: next.month },
|
||||||
|
overallScrollPercent: -1,
|
||||||
|
scrubberMonthScrollPercent: 0,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -422,7 +457,11 @@
|
|||||||
const next = segments[idx + 1];
|
const next = segments[idx + 1];
|
||||||
if (next) {
|
if (next) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
void onScrub?.({ year: next.year, month: next.month }, -1, 0);
|
void onScrub?.({
|
||||||
|
scrubberMonth: { year: next.year, month: next.month },
|
||||||
|
overallScrollPercent: -1,
|
||||||
|
scrubberMonthScrollPercent: 0,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,14 +110,19 @@
|
|||||||
let timelineElement: HTMLElement | undefined = $state();
|
let timelineElement: HTMLElement | undefined = $state();
|
||||||
let showSkeleton = $state(true);
|
let showSkeleton = $state(true);
|
||||||
let isShowSelectDate = $state(false);
|
let isShowSelectDate = $state(false);
|
||||||
let scrubberMonthPercent = $state(0);
|
// The percentage of scroll through the month that is currently intersecting the top boundary of the viewport.
|
||||||
let scrubberMonth: { year: number; month: number } | undefined = $state(undefined);
|
// Note: There may be multiple months visible within the viewport at any given time.
|
||||||
let scrubOverallPercent: number = $state(0);
|
let viewportTopMonthScrollPercent = $state(0);
|
||||||
|
// The timeline month intersecting the top position of the viewport
|
||||||
|
let viewportTopMonth: { year: number; month: number } | undefined = $state(undefined);
|
||||||
|
// Overall scroll percentage through the entire timeline (0-1)
|
||||||
|
let timelineScrollPercent: number = $state(0);
|
||||||
let scrubberWidth = $state(0);
|
let scrubberWidth = $state(0);
|
||||||
|
|
||||||
// 60 is the bottom spacer element at 60px
|
// 60 is the bottom spacer element at 60px
|
||||||
let bottomSectionHeight = 60;
|
let bottomSectionHeight = 60;
|
||||||
let leadout = $state(false);
|
// Indicates whether the viewport is currently in the lead-out section (after all months)
|
||||||
|
let isInLeadOutSection = $state(false);
|
||||||
|
|
||||||
const maxMd = $derived(mobileDevice.maxMd);
|
const maxMd = $derived(mobileDevice.maxMd);
|
||||||
const usingMobileDevice = $derived(mobileDevice.pointerCoarse);
|
const usingMobileDevice = $derived(mobileDevice.pointerCoarse);
|
||||||
@@ -301,20 +306,19 @@
|
|||||||
scrollTop(scrollToTop);
|
scrollTop(scrollToTop);
|
||||||
};
|
};
|
||||||
|
|
||||||
// note: don't throttle, debounch, or otherwise make this function async - it causes flicker
|
// note: don't throttle, debounce, or otherwise make this function async - it causes flicker
|
||||||
const onScrub: ScrubberListener = (
|
// this function scrolls the timeline to the specified month group and offset, based on scrubber interaction
|
||||||
scrubMonth: { year: number; month: number },
|
const onScrub: ScrubberListener = (scrubberData) => {
|
||||||
overallScrollPercent: number,
|
const { scrubberMonth, overallScrollPercent, scrubberMonthScrollPercent } = scrubberData;
|
||||||
scrubberMonthScrollPercent: number,
|
|
||||||
) => {
|
if (!scrubberMonth || timelineManager.timelineHeight < timelineManager.viewportHeight * 2) {
|
||||||
if (!scrubMonth || timelineManager.timelineHeight < timelineManager.viewportHeight * 2) {
|
|
||||||
// edge case - scroll limited due to size of content, must adjust - use use the overall percent instead
|
// edge case - scroll limited due to size of content, must adjust - use use the overall percent instead
|
||||||
const maxScroll = getMaxScroll();
|
const maxScroll = getMaxScroll();
|
||||||
const offset = maxScroll * overallScrollPercent;
|
const offset = maxScroll * overallScrollPercent;
|
||||||
scrollTop(offset);
|
scrollTop(offset);
|
||||||
} else {
|
} else {
|
||||||
const monthGroup = timelineManager.months.find(
|
const monthGroup = timelineManager.months.find(
|
||||||
({ yearMonth: { year, month } }) => year === scrubMonth.year && month === scrubMonth.month,
|
({ yearMonth: { year, month } }) => year === scrubberMonth.year && month === scrubberMonth.month,
|
||||||
);
|
);
|
||||||
if (!monthGroup) {
|
if (!monthGroup) {
|
||||||
return;
|
return;
|
||||||
@@ -325,7 +329,7 @@
|
|||||||
|
|
||||||
// note: don't throttle, debounch, or otherwise make this function async - it causes flicker
|
// note: don't throttle, debounch, or otherwise make this function async - it causes flicker
|
||||||
const handleTimelineScroll = () => {
|
const handleTimelineScroll = () => {
|
||||||
leadout = false;
|
isInLeadOutSection = false;
|
||||||
|
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return;
|
return;
|
||||||
@@ -334,19 +338,19 @@
|
|||||||
if (timelineManager.timelineHeight < timelineManager.viewportHeight * 2) {
|
if (timelineManager.timelineHeight < timelineManager.viewportHeight * 2) {
|
||||||
// edge case - scroll limited due to size of content, must adjust - use the overall percent instead
|
// edge case - scroll limited due to size of content, must adjust - use the overall percent instead
|
||||||
const maxScroll = getMaxScroll();
|
const maxScroll = getMaxScroll();
|
||||||
scrubOverallPercent = Math.min(1, element.scrollTop / maxScroll);
|
timelineScrollPercent = Math.min(1, element.scrollTop / maxScroll);
|
||||||
|
|
||||||
scrubberMonth = undefined;
|
viewportTopMonth = undefined;
|
||||||
scrubberMonthPercent = 0;
|
viewportTopMonthScrollPercent = 0;
|
||||||
} else {
|
} else {
|
||||||
let top = element.scrollTop;
|
let top = element.scrollTop;
|
||||||
if (top < timelineManager.topSectionHeight) {
|
if (top < timelineManager.topSectionHeight) {
|
||||||
// in the lead-in area
|
// in the lead-in area
|
||||||
scrubberMonth = undefined;
|
viewportTopMonth = undefined;
|
||||||
scrubberMonthPercent = 0;
|
viewportTopMonthScrollPercent = 0;
|
||||||
const maxScroll = getMaxScroll();
|
const maxScroll = getMaxScroll();
|
||||||
|
|
||||||
scrubOverallPercent = Math.min(1, element.scrollTop / maxScroll);
|
timelineScrollPercent = Math.min(1, element.scrollTop / maxScroll);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,15 +375,15 @@
|
|||||||
let next = top - monthGroupHeight * maxScrollPercent;
|
let next = top - monthGroupHeight * maxScrollPercent;
|
||||||
// instead of checking for < 0, add a little wiggle room for subpixel resolution
|
// instead of checking for < 0, add a little wiggle room for subpixel resolution
|
||||||
if (next < -1 && monthGroup) {
|
if (next < -1 && monthGroup) {
|
||||||
scrubberMonth = monthGroup;
|
viewportTopMonth = monthGroup;
|
||||||
|
|
||||||
// allowing next to be at least 1 may cause percent to go negative, so ensure positive percentage
|
// allowing next to be at least 1 may cause percent to go negative, so ensure positive percentage
|
||||||
scrubberMonthPercent = Math.max(0, top / (monthGroupHeight * maxScrollPercent));
|
viewportTopMonthScrollPercent = Math.max(0, top / (monthGroupHeight * maxScrollPercent));
|
||||||
|
|
||||||
// compensate for lost precision/rounding errors advance to the next bucket, if present
|
// compensate for lost precision/rounding errors advance to the next bucket, if present
|
||||||
if (scrubberMonthPercent > 0.9999 && i + 1 < monthsLength - 1) {
|
if (viewportTopMonthScrollPercent > 0.9999 && i + 1 < monthsLength - 1) {
|
||||||
scrubberMonth = timelineManager.months[i + 1].yearMonth;
|
viewportTopMonth = timelineManager.months[i + 1].yearMonth;
|
||||||
scrubberMonthPercent = 0;
|
viewportTopMonthScrollPercent = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
found = true;
|
found = true;
|
||||||
@@ -388,10 +392,10 @@
|
|||||||
top = next;
|
top = next;
|
||||||
}
|
}
|
||||||
if (!found) {
|
if (!found) {
|
||||||
leadout = true;
|
isInLeadOutSection = true;
|
||||||
scrubberMonth = undefined;
|
viewportTopMonth = undefined;
|
||||||
scrubberMonthPercent = 0;
|
viewportTopMonthScrollPercent = 0;
|
||||||
scrubOverallPercent = 1;
|
timelineScrollPercent = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -854,10 +858,10 @@
|
|||||||
height={timelineManager.viewportHeight}
|
height={timelineManager.viewportHeight}
|
||||||
timelineTopOffset={timelineManager.topSectionHeight}
|
timelineTopOffset={timelineManager.topSectionHeight}
|
||||||
timelineBottomOffset={bottomSectionHeight}
|
timelineBottomOffset={bottomSectionHeight}
|
||||||
{leadout}
|
{isInLeadOutSection}
|
||||||
{scrubOverallPercent}
|
{timelineScrollPercent}
|
||||||
{scrubberMonthPercent}
|
{viewportTopMonthScrollPercent}
|
||||||
{scrubberMonth}
|
{viewportTopMonth}
|
||||||
{onScrub}
|
{onScrub}
|
||||||
bind:scrubberWidth
|
bind:scrubberWidth
|
||||||
onScrubKeyDown={(evt) => {
|
onScrubKeyDown={(evt) => {
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ export type TimelineDateTime = TimelineDate & {
|
|||||||
millisecond: number;
|
millisecond: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ScrubberListener = (
|
export type ScrubberListener = (scrubberData: {
|
||||||
scrubberMonth: { year: number; month: number },
|
scrubberMonth: { year: number; month: number };
|
||||||
overallScrollPercent: number,
|
overallScrollPercent: number;
|
||||||
scrubberMonthScrollPercent: number,
|
scrubberMonthScrollPercent: number;
|
||||||
) => void | Promise<void>;
|
}) => void | Promise<void>;
|
||||||
|
|
||||||
// used for AssetResponseDto.dateTimeOriginal, amongst others
|
// used for AssetResponseDto.dateTimeOriginal, amongst others
|
||||||
export const fromISODateTime = (isoDateTime: string, timeZone: string): DateTime<true> =>
|
export const fromISODateTime = (isoDateTime: string, timeZone: string): DateTime<true> =>
|
||||||
|
|||||||
Reference in New Issue
Block a user