Compare commits
2 Commits
fix/async-
...
refactor/t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41066b1c31 | ||
|
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { afterNavigate, beforeNavigate, goto } from '$app/navigation';
|
import { afterNavigate, beforeNavigate } from '$app/navigation';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { resizeObserver, type OnResizeCallback } from '$lib/actions/resize-observer';
|
import { resizeObserver, type OnResizeCallback } from '$lib/actions/resize-observer';
|
||||||
import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut';
|
|
||||||
import type { Action } from '$lib/components/asset-viewer/actions/action';
|
import type { Action } from '$lib/components/asset-viewer/actions/action';
|
||||||
import type { AbsoluteResult, RelativeResult } from '$lib/components/shared-components/change-date.svelte';
|
|
||||||
import ChangeDate from '$lib/components/shared-components/change-date.svelte';
|
|
||||||
import Scrubber from '$lib/components/timeline/Scrubber.svelte';
|
import Scrubber from '$lib/components/timeline/Scrubber.svelte';
|
||||||
import {
|
import TimelineKeyboardActions from '$lib/components/timeline/actions/TimelineKeyboardActions.svelte';
|
||||||
setFocusToAsset as setFocusAssetInit,
|
import { AssetAction } from '$lib/constants';
|
||||||
setFocusTo as setFocusToInit,
|
|
||||||
} from '$lib/components/timeline/actions/focus-actions';
|
|
||||||
import { AppRoute, AssetAction } from '$lib/constants';
|
|
||||||
import HotModuleReload from '$lib/elements/HotModuleReload.svelte';
|
import HotModuleReload from '$lib/elements/HotModuleReload.svelte';
|
||||||
import Portal from '$lib/elements/Portal.svelte';
|
import Portal from '$lib/elements/Portal.svelte';
|
||||||
import Skeleton from '$lib/elements/Skeleton.svelte';
|
import Skeleton from '$lib/elements/Skeleton.svelte';
|
||||||
@@ -21,25 +15,17 @@
|
|||||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
||||||
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
|
||||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||||
import { showDeleteModal } from '$lib/stores/preferences.store';
|
import { updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions';
|
||||||
import { searchStore } from '$lib/stores/search.svelte';
|
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
|
||||||
import { handlePromiseError } from '$lib/utils';
|
|
||||||
import { deleteAssets, updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions';
|
|
||||||
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
|
||||||
import { navigate } from '$lib/utils/navigation';
|
import { navigate } from '$lib/utils/navigation';
|
||||||
import { getTimes, toTimelineAsset, type ScrubberListener, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
import { getTimes, toTimelineAsset, type ScrubberListener, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||||
import { AssetVisibility, getAssetInfo, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
import { getAssetInfo, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
||||||
import { modalManager } from '@immich/ui';
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { onMount, type Snippet } from 'svelte';
|
import { onMount, type Snippet } from 'svelte';
|
||||||
import type { UpdatePayload } from 'vite';
|
import type { UpdatePayload } from 'vite';
|
||||||
import DeleteAssetDialog from '../photos-page/delete-asset-dialog.svelte';
|
|
||||||
import TimelineDateGroup from './TimelineDateGroup.svelte';
|
import TimelineDateGroup from './TimelineDateGroup.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -109,16 +95,21 @@
|
|||||||
|
|
||||||
let timelineElement: HTMLElement | undefined = $state();
|
let timelineElement: HTMLElement | undefined = $state();
|
||||||
let showSkeleton = $state(true);
|
let showSkeleton = $state(true);
|
||||||
let isShowSelectDate = $state(false);
|
// The percentage of scroll through the month that is currently intersecting the top boundary of the viewport.
|
||||||
let scrubberMonthPercent = $state(0);
|
// Note: There may be multiple months visible within the viewport at any given time.
|
||||||
let scrubberMonth: { year: number; month: number } | undefined = $state(undefined);
|
let viewportTopMonthScrollPercent = $state(0);
|
||||||
let scrubOverallPercent: number = $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 isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
|
||||||
const maxMd = $derived(mobileDevice.maxMd);
|
const maxMd = $derived(mobileDevice.maxMd);
|
||||||
const usingMobileDevice = $derived(mobileDevice.pointerCoarse);
|
const usingMobileDevice = $derived(mobileDevice.pointerCoarse);
|
||||||
|
|
||||||
@@ -301,20 +292,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 +315,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 +324,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 +361,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,61 +378,14 @@
|
|||||||
top = next;
|
top = next;
|
||||||
}
|
}
|
||||||
if (!found) {
|
if (!found) {
|
||||||
leadout = true;
|
isInLeadOutSection = true;
|
||||||
scrubberMonth = undefined;
|
viewportTopMonth = undefined;
|
||||||
scrubberMonthPercent = 0;
|
viewportTopMonthScrollPercent = 0;
|
||||||
scrubOverallPercent = 1;
|
timelineScrollPercent = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const trashOrDelete = async (force: boolean = false) => {
|
|
||||||
isShowDeleteConfirmation = false;
|
|
||||||
await deleteAssets(
|
|
||||||
!(isTrashEnabled && !force),
|
|
||||||
(assetIds) => timelineManager.removeAssets(assetIds),
|
|
||||||
assetInteraction.selectedAssets,
|
|
||||||
!isTrashEnabled || force ? undefined : (assets) => timelineManager.addAssets(assets),
|
|
||||||
);
|
|
||||||
assetInteraction.clearMultiselect();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDelete = () => {
|
|
||||||
const hasTrashedAsset = assetInteraction.selectedAssets.some((asset) => asset.isTrashed);
|
|
||||||
|
|
||||||
if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) {
|
|
||||||
isShowDeleteConfirmation = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handlePromiseError(trashOrDelete(hasTrashedAsset));
|
|
||||||
};
|
|
||||||
|
|
||||||
const onForceDelete = () => {
|
|
||||||
if ($showDeleteModal) {
|
|
||||||
isShowDeleteConfirmation = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handlePromiseError(trashOrDelete(true));
|
|
||||||
};
|
|
||||||
|
|
||||||
const onStackAssets = async () => {
|
|
||||||
const result = await stackAssets(assetInteraction.selectedAssets);
|
|
||||||
|
|
||||||
updateStackedAssetInTimeline(timelineManager, result);
|
|
||||||
|
|
||||||
onEscape();
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleArchive = async () => {
|
|
||||||
const visibility = assetInteraction.isAllArchived ? AssetVisibility.Timeline : AssetVisibility.Archive;
|
|
||||||
const ids = await archiveAssets(assetInteraction.selectedAssets, visibility);
|
|
||||||
timelineManager.updateAssetOperation(ids, (asset) => {
|
|
||||||
asset.visibility = visibility;
|
|
||||||
return { remove: false };
|
|
||||||
});
|
|
||||||
deselectAllAssets();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectAsset = (asset: TimelineAsset) => {
|
const handleSelectAsset = (asset: TimelineAsset) => {
|
||||||
if (!timelineManager.albumAssets.has(asset.id)) {
|
if (!timelineManager.albumAssets.has(asset.id)) {
|
||||||
assetInteraction.selectAsset(asset);
|
assetInteraction.selectAsset(asset);
|
||||||
@@ -573,15 +516,7 @@
|
|||||||
|
|
||||||
let shiftKeyIsDown = $state(false);
|
let shiftKeyIsDown = $state(false);
|
||||||
|
|
||||||
const deselectAllAssets = () => {
|
|
||||||
cancelMultiselect(assetInteraction);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
if (searchStore.isSearchEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === 'Shift') {
|
if (event.key === 'Shift') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
shiftKeyIsDown = true;
|
shiftKeyIsDown = true;
|
||||||
@@ -589,16 +524,11 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onKeyUp = (event: KeyboardEvent) => {
|
const onKeyUp = (event: KeyboardEvent) => {
|
||||||
if (searchStore.isSearchEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === 'Shift') {
|
if (event.key === 'Shift') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
shiftKeyIsDown = false;
|
shiftKeyIsDown = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAssetCandidates = (asset: TimelineAsset | null) => {
|
const handleSelectAssetCandidates = (asset: TimelineAsset | null) => {
|
||||||
if (asset) {
|
if (asset) {
|
||||||
void selectAssetCandidates(asset);
|
void selectAssetCandidates(asset);
|
||||||
@@ -724,72 +654,6 @@
|
|||||||
assetInteraction.setAssetSelectionCandidates(assets);
|
assetInteraction.setAssetSelectionCandidates(assets);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelectStart = (e: Event) => {
|
|
||||||
if (assetInteraction.selectionActive && shiftKeyIsDown) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash);
|
|
||||||
let isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
|
|
||||||
let idsSelectedAssets = $derived(assetInteraction.selectedAssets.map(({ id }) => id));
|
|
||||||
let isShortcutModalOpen = false;
|
|
||||||
|
|
||||||
const handleOpenShortcutModal = async () => {
|
|
||||||
if (isShortcutModalOpen) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isShortcutModalOpen = true;
|
|
||||||
await modalManager.show(ShortcutsModal, {});
|
|
||||||
isShortcutModalOpen = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (isEmpty) {
|
|
||||||
assetInteraction.clearMultiselect();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const setFocusTo = setFocusToInit.bind(undefined, scrollToAsset, timelineManager);
|
|
||||||
const setFocusAsset = setFocusAssetInit.bind(undefined, scrollToAsset);
|
|
||||||
|
|
||||||
let shortcutList = $derived(
|
|
||||||
(() => {
|
|
||||||
if (searchStore.isSearchEnabled || $showAssetViewer) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const shortcuts: ShortcutOptions[] = [
|
|
||||||
{ shortcut: { key: 'Escape' }, onShortcut: onEscape },
|
|
||||||
{ shortcut: { key: '?', shift: true }, onShortcut: handleOpenShortcutModal },
|
|
||||||
{ shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) },
|
|
||||||
{ shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets(timelineManager, assetInteraction) },
|
|
||||||
{ shortcut: { key: 'ArrowRight' }, onShortcut: () => setFocusTo('earlier', 'asset') },
|
|
||||||
{ shortcut: { key: 'ArrowLeft' }, onShortcut: () => setFocusTo('later', 'asset') },
|
|
||||||
{ shortcut: { key: 'D' }, onShortcut: () => setFocusTo('earlier', 'day') },
|
|
||||||
{ shortcut: { key: 'D', shift: true }, onShortcut: () => setFocusTo('later', 'day') },
|
|
||||||
{ shortcut: { key: 'M' }, onShortcut: () => setFocusTo('earlier', 'month') },
|
|
||||||
{ shortcut: { key: 'M', shift: true }, onShortcut: () => setFocusTo('later', 'month') },
|
|
||||||
{ shortcut: { key: 'Y' }, onShortcut: () => setFocusTo('earlier', 'year') },
|
|
||||||
{ shortcut: { key: 'Y', shift: true }, onShortcut: () => setFocusTo('later', 'year') },
|
|
||||||
{ shortcut: { key: 'G' }, onShortcut: () => (isShowSelectDate = true) },
|
|
||||||
];
|
|
||||||
|
|
||||||
if (assetInteraction.selectionActive) {
|
|
||||||
shortcuts.push(
|
|
||||||
{ shortcut: { key: 'Delete' }, onShortcut: onDelete },
|
|
||||||
{ shortcut: { key: 'Delete', shift: true }, onShortcut: onForceDelete },
|
|
||||||
{ shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() },
|
|
||||||
{ shortcut: { key: 's' }, onShortcut: () => onStackAssets() },
|
|
||||||
{ shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return shortcuts;
|
|
||||||
})(),
|
|
||||||
);
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!lastAssetMouseEvent) {
|
if (!lastAssetMouseEvent) {
|
||||||
assetInteraction.clearAssetSelectionCandidates();
|
assetInteraction.clearAssetSelectionCandidates();
|
||||||
@@ -816,37 +680,17 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} onselectstart={onSelectStart} use:shortcuts={shortcutList} />
|
<svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} />
|
||||||
|
|
||||||
<HotModuleReload onAfterUpdate={handleAfterUpdate} onBeforeUpdate={handleBeforeUpdate} />
|
<HotModuleReload onAfterUpdate={handleAfterUpdate} onBeforeUpdate={handleBeforeUpdate} />
|
||||||
|
|
||||||
{#if isShowDeleteConfirmation}
|
<TimelineKeyboardActions
|
||||||
<DeleteAssetDialog
|
scrollToAsset={(asset) => scrollToAsset(asset) ?? false}
|
||||||
size={idsSelectedAssets.length}
|
{timelineManager}
|
||||||
onCancel={() => (isShowDeleteConfirmation = false)}
|
{assetInteraction}
|
||||||
onConfirm={() => handlePromiseError(trashOrDelete(true))}
|
bind:isShowDeleteConfirmation
|
||||||
/>
|
{onEscape}
|
||||||
{/if}
|
/>
|
||||||
|
|
||||||
{#if isShowSelectDate}
|
|
||||||
<ChangeDate
|
|
||||||
title="Navigate to Time"
|
|
||||||
initialDate={DateTime.now()}
|
|
||||||
timezoneInput={false}
|
|
||||||
onConfirm={async (dateString: AbsoluteResult | RelativeResult) => {
|
|
||||||
isShowSelectDate = false;
|
|
||||||
if (dateString.mode == 'absolute') {
|
|
||||||
const asset = await timelineManager.getClosestAssetToDate(
|
|
||||||
(DateTime.fromISO(dateString.date) as DateTime<true>).toObject(),
|
|
||||||
);
|
|
||||||
if (asset) {
|
|
||||||
setFocusAsset(asset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onCancel={() => (isShowSelectDate = false)}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if timelineManager.months.length > 0}
|
{#if timelineManager.months.length > 0}
|
||||||
<Scrubber
|
<Scrubber
|
||||||
@@ -854,10 +698,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) => {
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut';
|
||||||
|
import DeleteAssetDialog from '$lib/components/photos-page/delete-asset-dialog.svelte';
|
||||||
|
import ChangeDate, {
|
||||||
|
type AbsoluteResult,
|
||||||
|
type RelativeResult,
|
||||||
|
} from '$lib/components/shared-components/change-date.svelte';
|
||||||
|
import {
|
||||||
|
setFocusToAsset as setFocusAssetInit,
|
||||||
|
setFocusTo as setFocusToInit,
|
||||||
|
} from '$lib/components/timeline/actions/focus-actions';
|
||||||
|
import { AppRoute } from '$lib/constants';
|
||||||
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
|
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
||||||
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
|
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||||
|
import { searchStore } from '$lib/stores/search.svelte';
|
||||||
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
|
import { handlePromiseError } from '$lib/utils';
|
||||||
|
import { deleteAssets, updateStackedAssetInTimeline } from '$lib/utils/actions';
|
||||||
|
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
||||||
|
import { AssetVisibility } from '@immich/sdk';
|
||||||
|
import { modalManager } from '@immich/ui';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
timelineManager: TimelineManager;
|
||||||
|
assetInteraction: AssetInteraction;
|
||||||
|
isShowDeleteConfirmation: boolean;
|
||||||
|
onEscape?: () => void;
|
||||||
|
scrollToAsset: (asset: TimelineAsset) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
timelineManager = $bindable(),
|
||||||
|
assetInteraction,
|
||||||
|
isShowDeleteConfirmation = $bindable(false),
|
||||||
|
onEscape,
|
||||||
|
scrollToAsset,
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let isShowSelectDate = $state(false);
|
||||||
|
|
||||||
|
const trashOrDelete = async (force: boolean = false) => {
|
||||||
|
isShowDeleteConfirmation = false;
|
||||||
|
await deleteAssets(
|
||||||
|
!(isTrashEnabled && !force),
|
||||||
|
(assetIds) => timelineManager.removeAssets(assetIds),
|
||||||
|
assetInteraction.selectedAssets,
|
||||||
|
!isTrashEnabled || force ? undefined : (assets) => timelineManager.addAssets(assets),
|
||||||
|
);
|
||||||
|
assetInteraction.clearMultiselect();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDelete = () => {
|
||||||
|
const hasTrashedAsset = assetInteraction.selectedAssets.some((asset) => asset.isTrashed);
|
||||||
|
|
||||||
|
if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) {
|
||||||
|
isShowDeleteConfirmation = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handlePromiseError(trashOrDelete(hasTrashedAsset));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onForceDelete = () => {
|
||||||
|
if ($showDeleteModal) {
|
||||||
|
isShowDeleteConfirmation = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handlePromiseError(trashOrDelete(true));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStackAssets = async () => {
|
||||||
|
const result = await stackAssets(assetInteraction.selectedAssets);
|
||||||
|
|
||||||
|
updateStackedAssetInTimeline(timelineManager, result);
|
||||||
|
|
||||||
|
onEscape?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleArchive = async () => {
|
||||||
|
const visibility = assetInteraction.isAllArchived ? AssetVisibility.Timeline : AssetVisibility.Archive;
|
||||||
|
const ids = await archiveAssets(assetInteraction.selectedAssets, visibility);
|
||||||
|
timelineManager.updateAssetOperation(ids, (asset) => {
|
||||||
|
asset.visibility = visibility;
|
||||||
|
return { remove: false };
|
||||||
|
});
|
||||||
|
deselectAllAssets();
|
||||||
|
};
|
||||||
|
|
||||||
|
let shiftKeyIsDown = $state(false);
|
||||||
|
|
||||||
|
const deselectAllAssets = () => {
|
||||||
|
cancelMultiselect(assetInteraction);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (searchStore.isSearchEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Shift') {
|
||||||
|
event.preventDefault();
|
||||||
|
shiftKeyIsDown = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyUp = (event: KeyboardEvent) => {
|
||||||
|
if (searchStore.isSearchEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Shift') {
|
||||||
|
event.preventDefault();
|
||||||
|
shiftKeyIsDown = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelectStart = (e: Event) => {
|
||||||
|
if (assetInteraction.selectionActive && shiftKeyIsDown) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash);
|
||||||
|
const isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
|
||||||
|
const idsSelectedAssets = $derived(assetInteraction.selectedAssets.map(({ id }) => id));
|
||||||
|
let isShortcutModalOpen = false;
|
||||||
|
|
||||||
|
const handleOpenShortcutModal = async () => {
|
||||||
|
if (isShortcutModalOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isShortcutModalOpen = true;
|
||||||
|
await modalManager.show(ShortcutsModal, {});
|
||||||
|
isShortcutModalOpen = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (isEmpty) {
|
||||||
|
assetInteraction.clearMultiselect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const setFocusTo = setFocusToInit.bind(undefined, scrollToAsset, timelineManager);
|
||||||
|
const setFocusAsset = setFocusAssetInit.bind(undefined, scrollToAsset);
|
||||||
|
|
||||||
|
let shortcutList = $derived(
|
||||||
|
(() => {
|
||||||
|
if (searchStore.isSearchEnabled || $showAssetViewer) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const shortcuts: ShortcutOptions[] = [
|
||||||
|
{ shortcut: { key: '?', shift: true }, onShortcut: handleOpenShortcutModal },
|
||||||
|
{ shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) },
|
||||||
|
{ shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets(timelineManager, assetInteraction) },
|
||||||
|
{ shortcut: { key: 'ArrowRight' }, onShortcut: () => setFocusTo('earlier', 'asset') },
|
||||||
|
{ shortcut: { key: 'ArrowLeft' }, onShortcut: () => setFocusTo('later', 'asset') },
|
||||||
|
{ shortcut: { key: 'D' }, onShortcut: () => setFocusTo('earlier', 'day') },
|
||||||
|
{ shortcut: { key: 'D', shift: true }, onShortcut: () => setFocusTo('later', 'day') },
|
||||||
|
{ shortcut: { key: 'M' }, onShortcut: () => setFocusTo('earlier', 'month') },
|
||||||
|
{ shortcut: { key: 'M', shift: true }, onShortcut: () => setFocusTo('later', 'month') },
|
||||||
|
{ shortcut: { key: 'Y' }, onShortcut: () => setFocusTo('earlier', 'year') },
|
||||||
|
{ shortcut: { key: 'Y', shift: true }, onShortcut: () => setFocusTo('later', 'year') },
|
||||||
|
{ shortcut: { key: 'G' }, onShortcut: () => (isShowSelectDate = true) },
|
||||||
|
];
|
||||||
|
if (onEscape) {
|
||||||
|
shortcuts.push({ shortcut: { key: 'Escape' }, onShortcut: onEscape });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assetInteraction.selectionActive) {
|
||||||
|
shortcuts.push(
|
||||||
|
{ shortcut: { key: 'Delete' }, onShortcut: onDelete },
|
||||||
|
{ shortcut: { key: 'Delete', shift: true }, onShortcut: onForceDelete },
|
||||||
|
{ shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() },
|
||||||
|
{ shortcut: { key: 's' }, onShortcut: () => onStackAssets() },
|
||||||
|
{ shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return shortcuts;
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} onselectstart={onSelectStart} use:shortcuts={shortcutList} />
|
||||||
|
|
||||||
|
{#if isShowDeleteConfirmation}
|
||||||
|
<DeleteAssetDialog
|
||||||
|
size={idsSelectedAssets.length}
|
||||||
|
onCancel={() => (isShowDeleteConfirmation = false)}
|
||||||
|
onConfirm={() => handlePromiseError(trashOrDelete(true))}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if isShowSelectDate}
|
||||||
|
<ChangeDate
|
||||||
|
withDuration={false}
|
||||||
|
title="Navigate to Time"
|
||||||
|
initialDate={DateTime.now()}
|
||||||
|
timezoneInput={false}
|
||||||
|
onConfirm={async (dateString: AbsoluteResult | RelativeResult) => {
|
||||||
|
isShowSelectDate = false;
|
||||||
|
if (dateString.mode == 'absolute') {
|
||||||
|
const asset = await timelineManager.getClosestAssetToDate(
|
||||||
|
(DateTime.fromISO(dateString.date) as DateTime<true>).toObject(),
|
||||||
|
);
|
||||||
|
if (asset) {
|
||||||
|
setFocusAsset(asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onCancel={() => (isShowSelectDate = false)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
@@ -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