mirror of
https://github.com/immich-app/immich.git
synced 2026-05-13 11:02:15 -04:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f61b7a8a15 | |||
| c78b1d8ab4 | |||
| 94a34436a3 | |||
| 0eef15a3ab | |||
| 6982896549 | |||
| 2c812a2561 | |||
| 0b1188e42e | |||
| be20cd2bf9 |
@@ -37,7 +37,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N
|
||||
| `IMMICH_LOG_FORMAT` | Log output format (`console`, `json`) | `console` | server | api, microservices |
|
||||
| `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**<sup>\*2</sup>⚠️ | `/data` | server | api, microservices |
|
||||
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
|
||||
| `IMMICH_HELMET_FILE` | Path to a json file with [helmet](https://www.npmjs.com/package/helmet) options. Set to `false` to disable. Set to `true` to use `server/helmet.json`. | `false` | server | api, microservices |
|
||||
| `IMMICH_HELMET_FILE` | Path to a json file with [helmet](https://www.npmjs.com/package/helmet) options. Set to `false` to disable. Set to `true` to use `server/helmet.json`. | `false` | server | api |
|
||||
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
|
||||
| `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | |
|
||||
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
|
||||
|
||||
+1
-1
@@ -43,8 +43,8 @@ class BackgroundEngineLock(context: Context) : BackgroundWorkerLockApi, ImmichPl
|
||||
|
||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
super.onAttachedToEngine(binding)
|
||||
checkAndEnforceBackgroundLock(binding.applicationContext)
|
||||
engineCount.incrementAndGet()
|
||||
checkAndEnforceBackgroundLock(binding.applicationContext)
|
||||
Log.i(TAG, "Flutter engine attached. Attached Engines count: $engineCount")
|
||||
}
|
||||
|
||||
|
||||
+30
-4
@@ -5,8 +5,10 @@ import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkManager
|
||||
import io.flutter.embedding.engine.FlutterEngineCache
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -18,6 +20,7 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
||||
|
||||
override fun enable() {
|
||||
enqueueMediaObserver(ctx)
|
||||
enqueuePeriodicWorker(ctx)
|
||||
}
|
||||
|
||||
override fun saveNotificationMessage(title: String, body: String) {
|
||||
@@ -27,12 +30,14 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
||||
override fun configure(settings: BackgroundWorkerSettings) {
|
||||
BackgroundWorkerPreferences(ctx).updateSettings(settings)
|
||||
enqueueMediaObserver(ctx)
|
||||
enqueuePeriodicWorker(ctx)
|
||||
}
|
||||
|
||||
override fun disable() {
|
||||
WorkManager.getInstance(ctx).apply {
|
||||
cancelUniqueWork(OBSERVER_WORKER_NAME)
|
||||
cancelUniqueWork(BACKGROUND_WORKER_NAME)
|
||||
cancelUniqueWork(PERIODIC_WORKER_NAME)
|
||||
}
|
||||
Log.i(TAG, "Cancelled background upload tasks")
|
||||
}
|
||||
@@ -40,6 +45,7 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
||||
companion object {
|
||||
private const val BACKGROUND_WORKER_NAME = "immich/BackgroundWorkerV1"
|
||||
private const val OBSERVER_WORKER_NAME = "immich/MediaObserverV1"
|
||||
private const val PERIODIC_WORKER_NAME = "immich/PeriodicBackgroundWorkerV1"
|
||||
const val ENGINE_CACHE_KEY = "immich::background_worker::engine"
|
||||
|
||||
|
||||
@@ -55,7 +61,7 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
||||
setRequiresCharging(settings.requiresCharging)
|
||||
}.build()
|
||||
|
||||
val work = OneTimeWorkRequest.Builder(MediaObserver::class.java)
|
||||
val work = OneTimeWorkRequestBuilder<MediaObserver>()
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
WorkManager.getInstance(ctx)
|
||||
@@ -67,10 +73,30 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
|
||||
)
|
||||
}
|
||||
|
||||
fun enqueuePeriodicWorker(ctx: Context) {
|
||||
val settings = BackgroundWorkerPreferences(ctx).getSettings()
|
||||
val constraints = Constraints.Builder().apply {
|
||||
setRequiresCharging(settings.requiresCharging)
|
||||
}.build()
|
||||
|
||||
val work =
|
||||
PeriodicWorkRequestBuilder<PeriodicWorker>(
|
||||
1,
|
||||
TimeUnit.HOURS,
|
||||
15,
|
||||
TimeUnit.MINUTES
|
||||
).setConstraints(constraints)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(ctx)
|
||||
.enqueueUniquePeriodicWork(PERIODIC_WORKER_NAME, ExistingPeriodicWorkPolicy.UPDATE, work)
|
||||
|
||||
Log.i(TAG, "Enqueued periodic background worker with name: $PERIODIC_WORKER_NAME")
|
||||
}
|
||||
|
||||
fun enqueueBackgroundWorker(ctx: Context) {
|
||||
val constraints = Constraints.Builder().setRequiresBatteryNotLow(true).build()
|
||||
|
||||
val work = OneTimeWorkRequest.Builder(BackgroundWorker::class.java)
|
||||
val work = OneTimeWorkRequestBuilder<BackgroundWorker>()
|
||||
.setConstraints(constraints)
|
||||
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
|
||||
.build()
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package app.alextran.immich.background
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
|
||||
class PeriodicWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||
private val ctx: Context = context.applicationContext
|
||||
|
||||
override fun doWork(): Result {
|
||||
Log.i("PeriodicWorker", "Periodic worker triggered, starting background worker")
|
||||
BackgroundWorkerApiImpl.enqueueBackgroundWorker(ctx)
|
||||
return Result.success()
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,6 @@ material = { module = "com.google.android.material:material", version.ref = "mat
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
# TODO: update to version.ref = "kotlin" when background_downloader is removed
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version = "1.9.22" }
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version = "2.1.0" }
|
||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
|
||||
@@ -21,7 +21,7 @@ plugins {
|
||||
id "com.android.application" version "8.11.2" apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.2.20" apply false
|
||||
// TODO: update to match kotlin version when background_downloader is removed
|
||||
id "org.jetbrains.kotlin.plugin.serialization" version "1.9.22" apply false
|
||||
id "org.jetbrains.kotlin.plugin.serialization" version "2.1.0" apply false
|
||||
id "com.google.devtools.ksp" version "2.2.20-2.0.3" apply false
|
||||
}
|
||||
|
||||
|
||||
+18
-62
@@ -20,19 +20,21 @@ PODS:
|
||||
- Flutter
|
||||
- flutter_udid (0.0.1):
|
||||
- Flutter
|
||||
- SAMKeychain
|
||||
- flutter_web_auth_2 (3.0.0):
|
||||
- KeychainAccess
|
||||
- flutter_web_auth_2 (5.0.0):
|
||||
- Flutter
|
||||
- fluttertoast (0.0.2):
|
||||
- Flutter
|
||||
- geolocator_apple (1.2.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- home_widget (0.0.1):
|
||||
- Flutter
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- integration_test (0.0.1):
|
||||
- Flutter
|
||||
- KeychainAccess (4.2.2)
|
||||
- local_auth_darwin (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@@ -44,19 +46,13 @@ PODS:
|
||||
- Flutter
|
||||
- network_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- objective_c (0.0.1):
|
||||
- Flutter
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- photo_manager (3.7.1):
|
||||
- photo_manager (3.9.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- SAMKeychain (1.5.3)
|
||||
- share_handler_ios (0.0.14):
|
||||
- Flutter
|
||||
- share_handler_ios/share_handler_ios_models (= 0.0.14)
|
||||
@@ -70,28 +66,6 @@ PODS:
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqflite_darwin (0.0.4):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqlite3 (3.49.2):
|
||||
- sqlite3/common (= 3.49.2)
|
||||
- sqlite3/common (3.49.2)
|
||||
- sqlite3/dbstatvtab (3.49.2):
|
||||
- sqlite3/common
|
||||
- sqlite3/fts5 (3.49.2):
|
||||
- sqlite3/common
|
||||
- sqlite3/perf-threadsafe (3.49.2):
|
||||
- sqlite3/common
|
||||
- sqlite3/rtree (3.49.2):
|
||||
- sqlite3/common
|
||||
- sqlite3_flutter_libs (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqlite3 (~> 3.49.1)
|
||||
- sqlite3/dbstatvtab
|
||||
- sqlite3/fts5
|
||||
- sqlite3/perf-threadsafe
|
||||
- sqlite3/rtree
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
@@ -110,7 +84,7 @@ DEPENDENCIES:
|
||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||
- flutter_web_auth_2 (from `.symlinks/plugins/flutter_web_auth_2/ios`)
|
||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/ios`)
|
||||
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
|
||||
- home_widget (from `.symlinks/plugins/home_widget/ios`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||
@@ -118,25 +92,20 @@ DEPENDENCIES:
|
||||
- maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`)
|
||||
- native_video_player (from `.symlinks/plugins/native_video_player/ios`)
|
||||
- network_info_plus (from `.symlinks/plugins/network_info_plus/ios`)
|
||||
- objective_c (from `.symlinks/plugins/objective_c/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
||||
- photo_manager (from `.symlinks/plugins/photo_manager/darwin`)
|
||||
- share_handler_ios (from `.symlinks/plugins/share_handler_ios/ios`)
|
||||
- share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- KeychainAccess
|
||||
- MapLibre
|
||||
- SAMKeychain
|
||||
- sqlite3
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
background_downloader:
|
||||
@@ -164,7 +133,7 @@ EXTERNAL SOURCES:
|
||||
fluttertoast:
|
||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||
geolocator_apple:
|
||||
:path: ".symlinks/plugins/geolocator_apple/ios"
|
||||
:path: ".symlinks/plugins/geolocator_apple/darwin"
|
||||
home_widget:
|
||||
:path: ".symlinks/plugins/home_widget/ios"
|
||||
image_picker_ios:
|
||||
@@ -179,16 +148,12 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/native_video_player/ios"
|
||||
network_info_plus:
|
||||
:path: ".symlinks/plugins/network_info_plus/ios"
|
||||
objective_c:
|
||||
:path: ".symlinks/plugins/objective_c/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
permission_handler_apple:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
photo_manager:
|
||||
:path: ".symlinks/plugins/photo_manager/ios"
|
||||
:path: ".symlinks/plugins/photo_manager/darwin"
|
||||
share_handler_ios:
|
||||
:path: ".symlinks/plugins/share_handler_ios/ios"
|
||||
share_handler_ios_models:
|
||||
@@ -197,10 +162,6 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sqflite_darwin:
|
||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||
sqlite3_flutter_libs:
|
||||
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
wakelock_plus:
|
||||
@@ -216,32 +177,27 @@ SPEC CHECKSUMS:
|
||||
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
|
||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
||||
flutter_web_auth_2: 5c8d9dcd7848b5a9efb086d24e7a9adcae979c80
|
||||
flutter_udid: 92a5d31fe0526b7b6002a2318df702e12e7eb300
|
||||
flutter_web_auth_2: 646fc9df97a01c59e5eea99b237da2c6360f8439
|
||||
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
|
||||
geolocator_apple: 1560c3c875af2a412242c7a923e15d0d401966ff
|
||||
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
||||
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
|
||||
KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51
|
||||
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
||||
MapLibre: 69e572367f4ef6287e18246cfafc39c80cdcabcd
|
||||
maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f
|
||||
native_video_player: b65c58951ede2f93d103a25366bdebca95081265
|
||||
network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc
|
||||
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
photo_manager: 25fd77df14f4f0ba5ef99e2c61814dde77e2bceb
|
||||
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
|
||||
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1
|
||||
sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
|
||||
PODFILE CHECKSUM: 938abbae4114b9c2140c550a2a0d8f7c674f5dfe
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import BackgroundTasks
|
||||
import Flutter
|
||||
import native_video_player
|
||||
import network_info_plus
|
||||
import path_provider_foundation
|
||||
import permission_handler_apple
|
||||
import photo_manager
|
||||
import shared_preferences_foundation
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||
|
||||
@@ -150,6 +150,27 @@
|
||||
<array>
|
||||
<string>INSendMessageIntent</string>
|
||||
</array>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>flutter</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>FlutterSceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
@@ -176,27 +197,6 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>FlutterSceneDelegate</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>flutter</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>io.flutter.embedded_views_preview</key>
|
||||
|
||||
@@ -54,6 +54,9 @@ class _Executor extends Mixinable<_Executor> with _ExecutorLogger {
|
||||
var _dynamicSpawning = false;
|
||||
var _isolatesCount = numberOfProcessors;
|
||||
|
||||
@visibleForTesting
|
||||
UnmodifiableListView<Worker> get pool => UnmodifiableListView(_pool);
|
||||
|
||||
@override
|
||||
Future<void> init({int? isolatesCount, bool? dynamicSpawning}) async {
|
||||
if (_pool.isNotEmpty) {
|
||||
@@ -76,7 +79,9 @@ class _Executor extends Mixinable<_Executor> with _ExecutorLogger {
|
||||
Future<void> dispose() async {
|
||||
_queue.clear();
|
||||
for (final worker in _pool) {
|
||||
worker.kill();
|
||||
if (worker.initialized || worker.initializing) {
|
||||
worker.kill();
|
||||
}
|
||||
}
|
||||
_pool.clear();
|
||||
super.dispose();
|
||||
@@ -157,9 +162,7 @@ class _Executor extends Mixinable<_Executor> with _ExecutorLogger {
|
||||
_nextTaskId++;
|
||||
late final Task<R> task;
|
||||
final completer = Completer<R>();
|
||||
if (execution is Execute<R>) {
|
||||
task = TaskRegular<R>(id: id, workPriority: priority, execution: execution, completer: completer);
|
||||
} else if (execution is ExecuteWithPort<R>) {
|
||||
if (execution is ExecuteWithPort<R>) {
|
||||
task = TaskWithPort<R>(
|
||||
id: id,
|
||||
workPriority: priority,
|
||||
@@ -177,6 +180,8 @@ class _Executor extends Mixinable<_Executor> with _ExecutorLogger {
|
||||
completer: completer,
|
||||
onMessage: onMessage!,
|
||||
);
|
||||
} else if (execution is Execute<R>) {
|
||||
task = TaskRegular<R>(id: id, workPriority: priority, execution: execution, completer: completer);
|
||||
}
|
||||
_queue.add(task);
|
||||
_schedule();
|
||||
@@ -199,7 +204,7 @@ class _Executor extends Mixinable<_Executor> with _ExecutorLogger {
|
||||
if (_pool.every((worker) => worker.taskId != null)) {
|
||||
return;
|
||||
}
|
||||
if (_dynamicSpawning) {
|
||||
if (_dynamicSpawning && _queue.isNotEmpty) {
|
||||
final freeWorker = _pool.firstWhereOrNull(
|
||||
(worker) => worker.taskId == null && !worker.initialized && !worker.initializing,
|
||||
);
|
||||
@@ -221,7 +226,7 @@ class _Executor extends Mixinable<_Executor> with _ExecutorLogger {
|
||||
.work(task)
|
||||
.then(
|
||||
(value) {
|
||||
//could be completed already by cancel and it is normal.
|
||||
//might be completed by cancel and it is normal.
|
||||
//Assuming that worker finished with error and cleaned gracefully
|
||||
task.complete(value, null, null);
|
||||
},
|
||||
|
||||
@@ -24,13 +24,11 @@ class AssetMediaStatus {
|
||||
String toJson() => value;
|
||||
|
||||
static const created = AssetMediaStatus._(r'created');
|
||||
static const replaced = AssetMediaStatus._(r'replaced');
|
||||
static const duplicate = AssetMediaStatus._(r'duplicate');
|
||||
|
||||
/// List of all possible values in this [enum][AssetMediaStatus].
|
||||
static const values = <AssetMediaStatus>[
|
||||
created,
|
||||
replaced,
|
||||
duplicate,
|
||||
];
|
||||
|
||||
@@ -71,7 +69,6 @@ class AssetMediaStatusTypeTransformer {
|
||||
if (data != null) {
|
||||
switch (data) {
|
||||
case r'created': return AssetMediaStatus.created;
|
||||
case r'replaced': return AssetMediaStatus.replaced;
|
||||
case r'duplicate': return AssetMediaStatus.duplicate;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
|
||||
+139
-211
@@ -29,10 +29,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: "0c64e928dcbefddecd234205422bcfc2b5e6d31be0b86fef0d0dd48d7b4c9742"
|
||||
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.4"
|
||||
version: "4.0.9"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -45,10 +45,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: async
|
||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
version: "2.13.1"
|
||||
auto_route:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -69,12 +69,12 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: background_downloader
|
||||
sha256: a913b37cc47a656a225e9562b69576000d516f705482f392e2663500e6ff6032
|
||||
sha256: "4cb23d9ad4f5060944f38164e7b90d4bf99b57b2472a3bd4676e59b2db4afd06"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.3.0"
|
||||
version: "9.5.4"
|
||||
bonsoir:
|
||||
dependency: transitive
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
name: bonsoir
|
||||
sha256: "2e2cf3be580deccad9a48dcaddddf90de092e74b7de2015ef58fb24e11d66496"
|
||||
@@ -149,10 +149,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa"
|
||||
sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.4"
|
||||
version: "4.1.1"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -205,10 +205,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: checked_yaml
|
||||
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
|
||||
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
version: "2.0.4"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -261,10 +261,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: connectivity_plus_platform_interface
|
||||
sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204"
|
||||
sha256: "3c09627c536d22fd24691a905cdd8b14520de69da52c7a97499c8be5284a32ed"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "2.1.0"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -277,26 +277,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crop_image
|
||||
sha256: "4fdebd00d0c7d1a6e3abeb1e3843efbc202204b867f3e377fcebcf77aaf69a17"
|
||||
sha256: "27cbce1685a595efee62caab81c98b49b636f765c1da86353f58f5b2bf2775d8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.16"
|
||||
version: "1.0.17"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
|
||||
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.4+2"
|
||||
version: "0.3.5+2"
|
||||
crypto:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
version: "3.0.7"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -326,10 +326,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dbus
|
||||
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
|
||||
sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.11"
|
||||
version: "0.7.12"
|
||||
desktop_webview_window:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -342,10 +342,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: dd0e8e02186b2196c7848c9d394a5fd6e5b57a43a546082c5820b1ec72317e33
|
||||
sha256: b4fed1b2835da9d670d7bed7db79ae2a94b0f5ad6312268158a9b5479abbacdd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "12.2.0"
|
||||
version: "12.4.0"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -414,10 +414,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
|
||||
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.2.0"
|
||||
file:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -430,34 +430,34 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_linux
|
||||
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
|
||||
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+2"
|
||||
version: "0.9.4"
|
||||
file_selector_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_macos
|
||||
sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
|
||||
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4+2"
|
||||
version: "0.9.5"
|
||||
file_selector_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_platform_interface
|
||||
sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
|
||||
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.2"
|
||||
version: "2.7.0"
|
||||
file_selector_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_windows
|
||||
sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
|
||||
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+4"
|
||||
version: "0.9.3+5"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -471,14 +471,6 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_cache_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_cache_manager
|
||||
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.1"
|
||||
flutter_displaymode:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -557,10 +549,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: "5a1e6fb2c0561958d7e4c33574674bda7b77caaca7a33b758876956f2902eea3"
|
||||
sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.27"
|
||||
version: "2.0.34"
|
||||
flutter_riverpod:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -621,10 +613,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_svg
|
||||
sha256: b9c2ad5872518a27507ab432d1fb97e8813b05f0fc693f9d40fad06d073e0678
|
||||
sha256: "1ded017b39c8e15c8948ea855070a5ff8ff8b3d5e83f3446e02d6bb12add7ad9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
version: "2.2.4"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -634,26 +626,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_udid
|
||||
sha256: "166bee5989a58c66b8b62000ea65edccc7c8167bbafdbb08022638db330dd030"
|
||||
sha256: fc599671cbe8b328e509c961ec121880406ed994dde659cc9ece9c7503cd31c7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
version: "4.1.2"
|
||||
flutter_web_auth_2:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_web_auth_2
|
||||
sha256: "561c32d32ed537853de43852c35849cf1d37f3482f41f22b718ab6112f96b333"
|
||||
sha256: d354998934ddc338e69b999b2abaeb33c6fd09999d3a5f92ead1a6b49b49712e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0-alpha.0"
|
||||
version: "5.0.2"
|
||||
flutter_web_auth_2_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_web_auth_2_platform_interface
|
||||
sha256: "45927587ebb2364cd273675ec95f6f67b81725754b416cef2b65cdc63fd3e853"
|
||||
sha256: ba0fbba55bffb47242025f96852ad1ffba34bc451568f56ef36e613612baffab
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0-alpha.0"
|
||||
version: "5.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@@ -663,10 +655,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fluttertoast
|
||||
sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1"
|
||||
sha256: "90778fe0497fe3a09166e8cf2e0867310ff434b794526589e77ec03cf08ba8e8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.2.12"
|
||||
version: "8.2.14"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -700,18 +692,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geolocator_android
|
||||
sha256: "114072db5d1dce0ec0b36af2697f55c133bc89a2c8dd513e137c0afe59696ed4"
|
||||
sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.1+1"
|
||||
version: "5.0.2"
|
||||
geolocator_apple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geolocator_apple
|
||||
sha256: c4ecead17985ede9634f21500072edfcb3dba0ef7b97f8d7bc556d2d722b3ba3
|
||||
sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.9"
|
||||
version: "2.3.13"
|
||||
geolocator_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -724,10 +716,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geolocator_platform_interface
|
||||
sha256: "386ce3d9cce47838355000070b1d0b13efb5bc430f8ecda7e9238c8409ace012"
|
||||
sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.4"
|
||||
version: "4.2.6"
|
||||
geolocator_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -740,10 +732,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geolocator_windows
|
||||
sha256: "53da08937d07c24b0d9952eb57a3b474e29aae2abf9dd717f7e1230995f13f0e"
|
||||
sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.3"
|
||||
version: "0.2.5"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -812,10 +804,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
|
||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
version: "1.6.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -844,42 +836,42 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
|
||||
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.4"
|
||||
version: "4.8.0"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker
|
||||
sha256: "736eb56a911cf24d1859315ad09ddec0b66104bc41a7f8c5b96b4e2620cf5041"
|
||||
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.2.1"
|
||||
image_picker_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: "58a85e6f09fe9c4484d53d18a0bd6271b72c53fce1d05e6f745ae36d8c18efca"
|
||||
sha256: "66810af8e99b2657ee98e5c6f02064f69bb63f7a70e343937f70946c5f8c6622"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.13+5"
|
||||
version: "0.8.13+16"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_for_web
|
||||
sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6"
|
||||
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.1.1"
|
||||
image_picker_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_ios
|
||||
sha256: e675c22790bcc24e9abd455deead2b7a88de4b79f7327a281812f14de1a56f58
|
||||
sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.13+1"
|
||||
version: "0.8.13+6"
|
||||
image_picker_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -900,10 +892,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_platform_interface
|
||||
sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665"
|
||||
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
version: "2.11.1"
|
||||
image_picker_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -960,10 +952,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
version: "4.11.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1016,26 +1008,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_android
|
||||
sha256: "63ad7ca6396290626dc0cb34725a939e4cfe965d80d36112f08d49cf13a8136e"
|
||||
sha256: a0bdfcc0607050a26ef5b31d6b4b254581c3d3ce3c1816ab4d4f4a9173e84467
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.49"
|
||||
version: "1.0.56"
|
||||
local_auth_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_darwin
|
||||
sha256: "630996cd7b7f28f5ab92432c4b35d055dd03a747bc319e5ffbb3c4806a3e50d2"
|
||||
sha256: "699873970067a40ef2f2c09b4c72eb1cfef64224ef041b3df9fdc5c4c1f91f49"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.3"
|
||||
version: "1.6.1"
|
||||
local_auth_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_platform_interface
|
||||
sha256: "1b842ff177a7068442eae093b64abe3592f816afd2a533c0ebcdbe40f9d2075a"
|
||||
sha256: f98b8e388588583d3f781f6806e4f4c9f9e189d898d27f0c249b93a1973dd122
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.10"
|
||||
version: "1.1.0"
|
||||
local_auth_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1112,10 +1104,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: mocktail
|
||||
sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8"
|
||||
sha256: "5e1bf53cc7baa8062a33b84424deb61513858ea05c601b8509e683815b5914aa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
version: "1.0.5"
|
||||
native_toolchain_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1137,10 +1129,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: network_info_plus
|
||||
sha256: "08f4166bbb77da9e407edef6322a33f87b18c0ca46483fb25606cb3d2bfcdd2a"
|
||||
sha256: f926b2ba86aa0086a0dfbb9e5072089bc213d854135c1712f1d29fc89ba3c877
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.3"
|
||||
version: "6.1.4"
|
||||
network_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1161,10 +1153,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: objective_c
|
||||
sha256: "1f81ed9e41909d44162d7ec8663b2c647c202317cc0b56d3d56f6a13146a0b64"
|
||||
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.1.0"
|
||||
version: "9.3.0"
|
||||
octo_image:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1193,26 +1185,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67"
|
||||
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.2.0"
|
||||
package_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191"
|
||||
sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.3.0"
|
||||
version: "8.3.1"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c"
|
||||
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
version: "3.2.1"
|
||||
path:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1241,18 +1233,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12"
|
||||
sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.16"
|
||||
version: "2.2.23"
|
||||
path_provider_foundation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: efaec349ddfc181528345c56f8eda9d6cccd71c177511b132c6a0ddaefaa2738
|
||||
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.3"
|
||||
version: "2.6.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1297,10 +1289,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_apple
|
||||
sha256: f84a188e79a35c687c132a0a0556c254747a08561e99ab933f12f6ca71ef3c98
|
||||
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.4.6"
|
||||
version: "9.4.7"
|
||||
permission_handler_html:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1329,18 +1321,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
|
||||
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
version: "7.0.2"
|
||||
photo_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: photo_manager
|
||||
sha256: a0d9a7a9bc35eda02d33766412bde6d883a8b0acb86bbe37dac5f691a0894e8a
|
||||
sha256: fb3bc8ea653370f88742b3baa304700107c83d12748aa58b2b9f2ed3ef15e6c2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.7.1"
|
||||
version: "3.9.0"
|
||||
pigeon:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -1377,26 +1369,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
|
||||
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
version: "1.5.2"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: posix
|
||||
sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a
|
||||
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.1"
|
||||
version: "6.5.0"
|
||||
process:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: process
|
||||
sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d"
|
||||
sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.3"
|
||||
version: "5.0.5"
|
||||
protobuf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1445,14 +1437,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
rxdart:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: rxdart
|
||||
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.28.0"
|
||||
scroll_date_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1521,26 +1505,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a"
|
||||
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.2"
|
||||
version: "2.5.5"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "3ec7210872c4ba945e3244982918e502fa2bfb5230dff6832459ca0e1879b7ad"
|
||||
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.8"
|
||||
version: "2.4.23"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
|
||||
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.4"
|
||||
version: "2.5.6"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1553,10 +1537,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
||||
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
version: "2.4.2"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1635,54 +1619,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.2"
|
||||
sprintf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sprintf
|
||||
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
sqflite:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite
|
||||
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
sqflite_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_android
|
||||
sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.5"
|
||||
sqflite_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_darwin
|
||||
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
sqflite_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_platform_interface
|
||||
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
sqlcipher_flutter_libs:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1763,14 +1699,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: synchronized
|
||||
sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1815,10 +1743,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: universal_io
|
||||
sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad"
|
||||
sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
version: "2.3.1"
|
||||
universal_platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1839,34 +1767,34 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4"
|
||||
sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.15"
|
||||
version: "6.3.29"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626"
|
||||
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.2"
|
||||
version: "6.4.1"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
|
||||
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
version: "3.2.2"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
|
||||
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
version: "3.2.5"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1879,34 +1807,34 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9"
|
||||
sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
version: "2.4.2"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
|
||||
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
version: "3.1.5"
|
||||
uuid:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: uuid
|
||||
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
||||
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.1"
|
||||
version: "4.5.3"
|
||||
vector_graphics:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics
|
||||
sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de"
|
||||
sha256: "81da85e9ca8885ade47f9685b953cb098970d11be4821ac765580a6607ea4373"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.18"
|
||||
version: "1.1.21"
|
||||
vector_graphics_codec:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1919,10 +1847,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_compiler
|
||||
sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc
|
||||
sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.19"
|
||||
version: "1.2.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1935,10 +1863,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
||||
sha256: "046d3928e16fa4dc46e8350415661755ab759d9fc97fc21b5ab295f71e4f0499"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.0.0"
|
||||
version: "15.1.0"
|
||||
wakelock_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1951,10 +1879,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: wakelock_plus_platform_interface
|
||||
sha256: "036deb14cd62f558ca3b73006d52ce049fabcdcb2eddfe0bf0fe4e8a943b5cf2"
|
||||
sha256: "14b2e5b9e35c2631e656913c47adecdd71633ae92896a27a64c8f1fcfabc21cc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.5.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1999,10 +1927,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: b89e6e24d1454e149ab20fbb225af58660f0c0bf4475544650700d8e2da54aef
|
||||
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.11.0"
|
||||
version: "5.15.0"
|
||||
win32_registry:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -2023,10 +1951,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: worker_manager
|
||||
sha256: "1bce9f894a0c187856f5fc0e150e7fe1facce326f048ca6172947754dac3d4f3"
|
||||
sha256: "887587eb97e517bca88dea761bea96edc495513ec91e4c489dcf110967ba79ff"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.7"
|
||||
version: "7.2.9"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
+29
-26
@@ -9,37 +9,35 @@ environment:
|
||||
flutter: 3.41.6
|
||||
|
||||
dependencies:
|
||||
async: ^2.13.0
|
||||
async: ^2.13.1
|
||||
auto_route: ^11.1.0
|
||||
background_downloader: ^9.3.0
|
||||
background_downloader: ^9.5.4
|
||||
cast: ^2.1.0
|
||||
collection: ^1.19.1
|
||||
connectivity_plus: ^6.1.3
|
||||
crop_image: ^1.0.16
|
||||
crypto: ^3.0.6
|
||||
device_info_plus: ^12.2.0
|
||||
# DB
|
||||
connectivity_plus: ^6.1.5
|
||||
crop_image: ^1.0.17
|
||||
crypto: ^3.0.7
|
||||
device_info_plus: ^12.4.0
|
||||
drift: ^2.32.1
|
||||
drift_flutter: ^0.3.0
|
||||
dynamic_color: ^1.8.1
|
||||
easy_localization: ^3.0.8
|
||||
ffi: ^2.1.4
|
||||
ffi: ^2.2.0
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_cache_manager: ^3.4.1
|
||||
flutter_displaymode: ^0.7.0
|
||||
flutter_hooks: ^0.21.3+1
|
||||
flutter_local_notifications: ^17.2.1+2
|
||||
flutter_local_notifications: ^17.2.4
|
||||
flutter_secure_storage: ^9.2.4
|
||||
flutter_svg: ^2.2.1
|
||||
flutter_udid: ^4.0.0
|
||||
flutter_web_auth_2: ^5.0.0-alpha.0
|
||||
fluttertoast: ^8.2.12
|
||||
flutter_svg: ^2.2.4
|
||||
flutter_udid: ^4.1.2
|
||||
flutter_web_auth_2: ^5.0.2
|
||||
fluttertoast: ^8.2.14
|
||||
geolocator: ^14.0.2
|
||||
home_widget: ^0.8.1
|
||||
hooks_riverpod: ^2.6.1
|
||||
http: ^1.5.0
|
||||
image_picker: ^1.2.0
|
||||
http: ^1.6.0
|
||||
image_picker: ^1.2.1
|
||||
immich_ui:
|
||||
path: './packages/ui'
|
||||
intl: ^0.20.2
|
||||
@@ -50,16 +48,16 @@ dependencies:
|
||||
git:
|
||||
url: https://github.com/immich-app/native_video_player
|
||||
ref: 'cdf621bdb7edaf996e118a58a48f6441187d79c6'
|
||||
network_info_plus: ^6.1.3
|
||||
network_info_plus: ^6.1.4
|
||||
octo_image: ^2.1.0
|
||||
openapi:
|
||||
path: openapi
|
||||
package_info_plus: ^8.3.0
|
||||
package_info_plus: ^8.3.1
|
||||
path: ^1.9.1
|
||||
path_provider: ^2.1.5
|
||||
path_provider_foundation: ^2.4.3
|
||||
path_provider_foundation: ^2.6.0
|
||||
permission_handler: ^11.4.0
|
||||
photo_manager: ^3.7.1
|
||||
photo_manager: ^3.9.0
|
||||
pinput: ^5.0.2
|
||||
punycode: ^1.0.0
|
||||
scroll_date_picker: ^3.8.0
|
||||
@@ -71,9 +69,9 @@ dependencies:
|
||||
thumbhash: 0.1.0+1
|
||||
timezone: ^0.9.4
|
||||
url_launcher: ^6.3.2
|
||||
uuid: ^4.5.1
|
||||
wakelock_plus: ^1.3.0
|
||||
worker_manager: ^7.2.7
|
||||
uuid: ^4.5.3
|
||||
wakelock_plus: ^1.3.3
|
||||
worker_manager: ^7.2.9
|
||||
web_socket: ^1.0.1
|
||||
socket_io_client:
|
||||
git:
|
||||
@@ -92,7 +90,7 @@ dependencies:
|
||||
|
||||
dev_dependencies:
|
||||
auto_route_generator: ^10.5.0
|
||||
build_runner: ^2.4.8
|
||||
build_runner: ^2.13.1
|
||||
# Drift generator
|
||||
drift_dev: ^2.32.1
|
||||
fake_async: ^1.3.3
|
||||
@@ -104,9 +102,14 @@ dev_dependencies:
|
||||
sdk: flutter
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
mocktail: ^1.0.4
|
||||
mocktail: ^1.0.5
|
||||
# Type safe platform code
|
||||
pigeon: ^26.0.2
|
||||
pigeon: ^26.3.4
|
||||
|
||||
# cast 2.1.0 declares a loose bonsoir range but its code targets the 5.x API.
|
||||
# Pin bonsoir to 5.x until cast releases a version compatible with bonsoir 6.x.
|
||||
dependency_overrides:
|
||||
bonsoir: ^5.1.11
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
||||
@@ -16348,7 +16348,6 @@
|
||||
"description": "Upload status",
|
||||
"enum": [
|
||||
"created",
|
||||
"replaced",
|
||||
"duplicate"
|
||||
],
|
||||
"type": "string"
|
||||
|
||||
@@ -6899,7 +6899,6 @@ export enum Permission {
|
||||
}
|
||||
export enum AssetMediaStatus {
|
||||
Created = "created",
|
||||
Replaced = "replaced",
|
||||
Duplicate = "duplicate"
|
||||
}
|
||||
export enum AssetUploadAction {
|
||||
|
||||
Generated
+122
-40
@@ -67,7 +67,7 @@ importers:
|
||||
version: 24.12.2
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^4.0.0
|
||||
version: 4.1.2(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))
|
||||
version: 4.1.2(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))
|
||||
byte-size:
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.1
|
||||
@@ -112,10 +112,10 @@ importers:
|
||||
version: 8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vitest:
|
||||
specifier: ^4.0.0
|
||||
version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest-fetch-mock:
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.5(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))
|
||||
version: 0.4.5(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))
|
||||
yaml:
|
||||
specifier: ^2.3.1
|
||||
version: 2.8.3
|
||||
@@ -412,9 +412,6 @@ importers:
|
||||
'@socket.io/redis-adapter':
|
||||
specifier: ^8.3.0
|
||||
version: 8.3.0(socket.io-adapter@2.5.6)
|
||||
ajv:
|
||||
specifier: ^8.17.1
|
||||
version: 8.18.0
|
||||
archiver:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.1
|
||||
@@ -523,9 +520,6 @@ importers:
|
||||
pg:
|
||||
specifier: ^8.11.3
|
||||
version: 8.20.0
|
||||
pg-connection-string:
|
||||
specifier: ^2.9.1
|
||||
version: 2.12.0
|
||||
picomatch:
|
||||
specifier: ^4.0.2
|
||||
version: 4.0.4
|
||||
@@ -670,7 +664,7 @@ importers:
|
||||
version: 13.15.10
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.32.0)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.32.0)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
eslint:
|
||||
specifier: ^10.0.0
|
||||
version: 10.1.0(jiti@2.6.1)
|
||||
@@ -689,9 +683,6 @@ importers:
|
||||
mock-fs:
|
||||
specifier: ^5.2.0
|
||||
version: 5.5.0
|
||||
node-gyp:
|
||||
specifier: ^12.0.0
|
||||
version: 12.2.0
|
||||
pngjs:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
@@ -727,7 +718,7 @@ importers:
|
||||
version: 6.1.1(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest:
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.32.0)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.32.0)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
web:
|
||||
dependencies:
|
||||
@@ -784,7 +775,7 @@ importers:
|
||||
version: 2.6.0
|
||||
fabric:
|
||||
specifier: ^7.0.0
|
||||
version: 7.2.0(encoding@0.1.13)
|
||||
version: 7.2.0
|
||||
geo-coordinates-parser:
|
||||
specifier: ^1.7.4
|
||||
version: 1.7.4
|
||||
@@ -890,7 +881,7 @@ importers:
|
||||
version: 6.9.1
|
||||
'@testing-library/svelte':
|
||||
specifier: ^5.2.8
|
||||
version: 5.3.1(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))
|
||||
version: 5.3.1(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))
|
||||
'@testing-library/user-event':
|
||||
specifier: ^14.5.2
|
||||
version: 14.6.1(@testing-library/dom@10.4.1)
|
||||
@@ -914,7 +905,7 @@ importers:
|
||||
version: 1.5.6
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^4.0.0
|
||||
version: 4.1.2(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))
|
||||
version: 4.1.2(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))
|
||||
dotenv:
|
||||
specifier: ^17.0.0
|
||||
version: 17.3.1
|
||||
@@ -958,8 +949,8 @@ importers:
|
||||
specifier: 5.55.1
|
||||
version: 5.55.1
|
||||
svelte-check:
|
||||
specifier: ^4.1.5
|
||||
version: 4.4.5(picomatch@4.0.4)(svelte@5.55.1)(typescript@6.0.2)
|
||||
specifier: ^4.4.6
|
||||
version: 4.4.6(picomatch@4.0.4)(svelte@5.55.1)(typescript@6.0.2)
|
||||
svelte-eslint-parser:
|
||||
specifier: ^1.3.3
|
||||
version: 1.6.0(svelte@5.55.1)
|
||||
@@ -977,7 +968,7 @@ importers:
|
||||
version: 8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vitest:
|
||||
specifier: ^4.0.0
|
||||
version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
packages:
|
||||
|
||||
@@ -11328,8 +11319,8 @@ packages:
|
||||
peerDependencies:
|
||||
svelte: '>= 3.43.1 < 6'
|
||||
|
||||
svelte-check@4.4.5:
|
||||
resolution: {integrity: sha512-1bSwIRCvvmSHrlK52fOlZmVtUZgil43jNL/2H18pRpa+eQjzGt6e3zayxhp1S7GajPFKNM/2PMCG+DZFHlG9fw==}
|
||||
svelte-check@4.4.6:
|
||||
resolution: {integrity: sha512-kP1zG81EWaFe9ZyTv4ZXv44Csi6Pkdpb7S3oj6m+K2ec/IcDg/a8LsFsnVLqm2nxtkSwsd5xPj/qFkTBgXHXjg==}
|
||||
engines: {node: '>= 18.0.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -15556,6 +15547,22 @@ snapshots:
|
||||
|
||||
'@mapbox/mapbox-gl-rtl-text@0.3.0': {}
|
||||
|
||||
'@mapbox/node-pre-gyp@1.0.11':
|
||||
dependencies:
|
||||
detect-libc: 2.1.2
|
||||
https-proxy-agent: 5.0.1
|
||||
make-dir: 3.1.0
|
||||
node-fetch: 2.7.0
|
||||
nopt: 5.0.0
|
||||
npmlog: 5.0.1
|
||||
rimraf: 3.0.2
|
||||
semver: 7.7.4
|
||||
tar: 6.2.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
'@mapbox/node-pre-gyp@1.0.11(encoding@0.1.13)':
|
||||
dependencies:
|
||||
detect-libc: 2.1.2
|
||||
@@ -16943,14 +16950,14 @@ snapshots:
|
||||
dependencies:
|
||||
svelte: 5.55.1
|
||||
|
||||
'@testing-library/svelte@5.3.1(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))':
|
||||
'@testing-library/svelte@5.3.1(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))':
|
||||
dependencies:
|
||||
'@testing-library/dom': 10.4.1
|
||||
'@testing-library/svelte-core': 1.0.0(svelte@5.55.1)
|
||||
svelte: 5.55.1
|
||||
optionalDependencies:
|
||||
vite: 8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
'@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)':
|
||||
dependencies:
|
||||
@@ -17645,7 +17652,7 @@ snapshots:
|
||||
|
||||
'@vercel/oidc@3.0.5': {}
|
||||
|
||||
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.32.0)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.32.0)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
@@ -17660,11 +17667,11 @@ snapshots:
|
||||
std-env: 3.10.0
|
||||
test-exclude: 7.0.2
|
||||
tinyrainbow: 2.0.0
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.32.0)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.32.0)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/coverage-v8@4.1.2(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))':
|
||||
'@vitest/coverage-v8@4.1.2(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))':
|
||||
dependencies:
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
'@vitest/utils': 4.1.2
|
||||
@@ -17676,9 +17683,9 @@ snapshots:
|
||||
obug: 2.1.1
|
||||
std-env: 4.0.0
|
||||
tinyrainbow: 3.1.0
|
||||
vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
'@vitest/coverage-v8@4.1.2(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))':
|
||||
'@vitest/coverage-v8@4.1.2(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))':
|
||||
dependencies:
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
'@vitest/utils': 4.1.2
|
||||
@@ -17690,7 +17697,7 @@ snapshots:
|
||||
obug: 2.1.1
|
||||
std-env: 4.0.0
|
||||
tinyrainbow: 3.1.0
|
||||
vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
'@vitest/expect@3.2.4':
|
||||
dependencies:
|
||||
@@ -18466,6 +18473,16 @@ snapshots:
|
||||
|
||||
caniuse-lite@1.0.30001776: {}
|
||||
|
||||
canvas@2.11.2:
|
||||
dependencies:
|
||||
'@mapbox/node-pre-gyp': 1.0.11
|
||||
nan: 2.26.2
|
||||
simple-get: 3.1.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
canvas@2.11.2(encoding@0.1.13):
|
||||
dependencies:
|
||||
'@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13)
|
||||
@@ -20097,10 +20114,10 @@ snapshots:
|
||||
|
||||
extend@3.0.2: {}
|
||||
|
||||
fabric@7.2.0(encoding@0.1.13):
|
||||
fabric@7.2.0:
|
||||
optionalDependencies:
|
||||
canvas: 2.11.2(encoding@0.1.13)
|
||||
jsdom: 26.1.0(canvas@2.11.2(encoding@0.1.13))
|
||||
canvas: 2.11.2
|
||||
jsdom: 26.1.0(canvas@2.11.2)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- encoding
|
||||
@@ -21267,6 +21284,36 @@ snapshots:
|
||||
- utf-8-validate
|
||||
optional: true
|
||||
|
||||
jsdom@26.1.0(canvas@2.11.2):
|
||||
dependencies:
|
||||
cssstyle: 4.6.0
|
||||
data-urls: 5.0.0
|
||||
decimal.js: 10.6.0
|
||||
html-encoding-sniffer: 4.0.0
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
is-potential-custom-element-name: 1.0.1
|
||||
nwsapi: 2.2.23
|
||||
parse5: 7.3.0
|
||||
rrweb-cssom: 0.8.0
|
||||
saxes: 6.0.0
|
||||
symbol-tree: 3.2.4
|
||||
tough-cookie: 5.1.2
|
||||
w3c-xmlserializer: 5.0.0
|
||||
webidl-conversions: 7.0.0
|
||||
whatwg-encoding: 3.1.1
|
||||
whatwg-mimetype: 4.0.0
|
||||
whatwg-url: 14.2.0
|
||||
ws: 8.20.0
|
||||
xml-name-validator: 5.0.0
|
||||
optionalDependencies:
|
||||
canvas: 2.11.2
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
optional: true
|
||||
|
||||
jsep@1.4.0: {}
|
||||
|
||||
jsesc@3.1.0: {}
|
||||
@@ -22508,6 +22555,11 @@ snapshots:
|
||||
emojilib: 2.4.0
|
||||
skin-tone: 2.0.0
|
||||
|
||||
node-fetch@2.7.0:
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
optional: true
|
||||
|
||||
node-fetch@2.7.0(encoding@0.1.13):
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
@@ -24744,7 +24796,7 @@ snapshots:
|
||||
dependencies:
|
||||
svelte: 5.55.1
|
||||
|
||||
svelte-check@4.4.5(picomatch@4.0.4)(svelte@5.55.1)(typescript@6.0.2):
|
||||
svelte-check@4.4.6(picomatch@4.0.4)(svelte@5.55.1)(typescript@6.0.2):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
chokidar: 4.0.3
|
||||
@@ -25648,11 +25700,11 @@ snapshots:
|
||||
optionalDependencies:
|
||||
vite: 8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
vitest-fetch-mock@0.4.5(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))):
|
||||
vitest-fetch-mock@0.4.5(vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))):
|
||||
dependencies:
|
||||
vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest: 4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.32.0)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3):
|
||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.32.0)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/expect': 3.2.4
|
||||
@@ -25681,7 +25733,7 @@ snapshots:
|
||||
'@types/debug': 4.1.12
|
||||
'@types/node': 24.12.2
|
||||
happy-dom: 20.8.9
|
||||
jsdom: 26.1.0(canvas@2.11.2(encoding@0.1.13))
|
||||
jsdom: 26.1.0(canvas@2.11.2)
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
- less
|
||||
@@ -25726,7 +25778,37 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- msw
|
||||
|
||||
vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@24.12.2)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.1.2
|
||||
'@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
'@vitest/pretty-format': 4.1.2
|
||||
'@vitest/runner': 4.1.2
|
||||
'@vitest/snapshot': 4.1.2
|
||||
'@vitest/spy': 4.1.2
|
||||
'@vitest/utils': 4.1.2
|
||||
es-module-lexer: 2.0.0
|
||||
expect-type: 1.3.0
|
||||
magic-string: 0.30.21
|
||||
obug: 2.1.1
|
||||
pathe: 2.0.3
|
||||
picomatch: 4.0.4
|
||||
std-env: 4.0.0
|
||||
tinybench: 2.9.0
|
||||
tinyexec: 1.0.4
|
||||
tinyglobby: 0.2.15
|
||||
tinyrainbow: 3.1.0
|
||||
vite: 8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@24.12.2)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@types/node': 24.12.2
|
||||
happy-dom: 20.8.9
|
||||
jsdom: 26.1.0(canvas@2.11.2)
|
||||
transitivePeerDependencies:
|
||||
- msw
|
||||
|
||||
vitest@4.1.2(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(happy-dom@20.8.9)(jsdom@26.1.0(canvas@2.11.2))(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.1.2
|
||||
'@vitest/mocker': 4.1.2(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
@@ -25752,7 +25834,7 @@ snapshots:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@types/node': 25.5.0
|
||||
happy-dom: 20.8.9
|
||||
jsdom: 26.1.0(canvas@2.11.2(encoding@0.1.13))
|
||||
jsdom: 26.1.0(canvas@2.11.2)
|
||||
transitivePeerDependencies:
|
||||
- msw
|
||||
|
||||
|
||||
+1
-5
@@ -26,7 +26,6 @@
|
||||
"test": "vitest --config test/vitest.config.mjs",
|
||||
"test:cov": "vitest --config test/vitest.config.mjs --coverage",
|
||||
"test:medium": "vitest --config test/vitest.config.medium.mjs",
|
||||
"typeorm": "typeorm",
|
||||
"migrations:debug": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations generate --debug",
|
||||
"migrations:generate": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations generate",
|
||||
"migrations:create": "sql-tools -u ${DB_URL:-postgres://postgres:postgres@localhost:5432/immich} migrations create",
|
||||
@@ -63,7 +62,6 @@
|
||||
"@react-email/components": "^1.0.0",
|
||||
"@react-email/render": "^2.0.0",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"ajv": "^8.17.1",
|
||||
"archiver": "^7.0.0",
|
||||
"async-lock": "^1.4.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
@@ -96,11 +94,10 @@
|
||||
"nestjs-cls": "^6.0.0",
|
||||
"nestjs-kysely": "3.1.2",
|
||||
"nestjs-otel": "^7.0.0",
|
||||
"nodemailer": "^8.0.0",
|
||||
"nestjs-zod": "^5.3.0",
|
||||
"nodemailer": "^8.0.0",
|
||||
"openid-client": "^6.3.3",
|
||||
"pg": "^8.11.3",
|
||||
"pg-connection-string": "^2.9.1",
|
||||
"picomatch": "^4.0.2",
|
||||
"postgres": "3.4.8",
|
||||
"react": "^19.0.0",
|
||||
@@ -157,7 +154,6 @@
|
||||
"eslint-plugin-unicorn": "^64.0.0",
|
||||
"globals": "^17.0.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"node-gyp": "^12.0.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Duration } from 'luxon';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { SemVer } from 'semver';
|
||||
@@ -52,9 +51,6 @@ const packageFile = join(basePath, '..', 'package.json');
|
||||
const { version } = JSON.parse(readFileSync(packageFile, 'utf8'));
|
||||
export const serverVersion = new SemVer(version);
|
||||
|
||||
export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 });
|
||||
export const ONE_HOUR = Duration.fromObject({ hours: 1 });
|
||||
|
||||
export const citiesFile = 'cities500.txt';
|
||||
export const reverseGeocodeMaxDistance = 25_000;
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import { AlbumTable } from 'src/schema/tables/album.table';
|
||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
import { PluginActionTable, PluginFilterTable, PluginTable } from 'src/schema/tables/plugin.table';
|
||||
import { PluginActionTable, PluginFilterTable } from 'src/schema/tables/plugin.table';
|
||||
import { WorkflowActionTable, WorkflowFilterTable, WorkflowTable } from 'src/schema/tables/workflow.table';
|
||||
import { UserMetadataItem } from 'src/types';
|
||||
import type { ActionConfig, FilterConfig, JSONSchema } from 'src/types/plugin-schema.types';
|
||||
@@ -277,8 +277,6 @@ export type AssetFace = {
|
||||
isVisible: boolean;
|
||||
};
|
||||
|
||||
export type Plugin = Selectable<PluginTable>;
|
||||
|
||||
export type PluginFilter = Selectable<PluginFilterTable> & {
|
||||
methodName: string;
|
||||
title: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BeforeUpdateTrigger, Column, ColumnOptions } from '@immich/sql-tools';
|
||||
import { SetMetadata, applyDecorators } from '@nestjs/common';
|
||||
import { ApiOperation, ApiOperationOptions, ApiProperty, ApiPropertyOptions, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiOperation, ApiOperationOptions, ApiTags } from '@nestjs/swagger';
|
||||
import _ from 'lodash';
|
||||
import { ApiCustomExtension, ApiTag, ImmichWorker, JobName, MetadataKey, QueueName } from 'src/enum';
|
||||
import { EmitEvent } from 'src/repositories/event.repository';
|
||||
@@ -172,17 +172,6 @@ export const Endpoint = ({ history, ...options }: EndpointOptions) => {
|
||||
return applyDecorators(...decorators);
|
||||
};
|
||||
|
||||
export type PropertyOptions = ApiPropertyOptions & { history?: HistoryBuilder };
|
||||
export const Property = ({ history, ...options }: PropertyOptions) => {
|
||||
const extensions = history?.getExtensions() ?? {};
|
||||
|
||||
if (history?.isDeprecated()) {
|
||||
options.deprecated = true;
|
||||
}
|
||||
|
||||
return ApiProperty({ ...options, ...extensions });
|
||||
};
|
||||
|
||||
type HistoryEntry = {
|
||||
version: string;
|
||||
state: ApiState | 'Added' | 'Updated';
|
||||
|
||||
@@ -3,7 +3,6 @@ import z from 'zod';
|
||||
|
||||
export enum AssetMediaStatus {
|
||||
CREATED = 'created',
|
||||
REPLACED = 'replaced',
|
||||
DUPLICATE = 'duplicate',
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@ export enum AuthType {
|
||||
OAuth = 'oauth',
|
||||
}
|
||||
|
||||
export const AuthTypeSchema = z.enum(AuthType).describe('Auth type').meta({ id: 'AuthType' });
|
||||
|
||||
export enum ImmichCookie {
|
||||
AccessToken = 'immich_access_token',
|
||||
MaintenanceToken = 'immich_maintenance_token',
|
||||
@@ -17,8 +15,6 @@ export enum ImmichCookie {
|
||||
OAuthCodeVerifier = 'immich_oauth_code_verifier',
|
||||
}
|
||||
|
||||
export const ImmichCookieSchema = z.enum(ImmichCookie).describe('Immich cookie').meta({ id: 'ImmichCookie' });
|
||||
|
||||
export enum ImmichHeader {
|
||||
ApiKey = 'x-api-key',
|
||||
UserToken = 'x-immich-user-token',
|
||||
@@ -29,8 +25,6 @@ export enum ImmichHeader {
|
||||
Cid = 'x-immich-cid',
|
||||
}
|
||||
|
||||
export const ImmichHeaderSchema = z.enum(ImmichHeader).describe('Immich header').meta({ id: 'ImmichHeader' });
|
||||
|
||||
export enum ImmichQuery {
|
||||
SharedLinkKey = 'key',
|
||||
SharedLinkSlug = 'slug',
|
||||
@@ -38,8 +32,6 @@ export enum ImmichQuery {
|
||||
SessionKey = 'sessionKey',
|
||||
}
|
||||
|
||||
export const ImmichQuerySchema = z.enum(ImmichQuery).describe('Immich query').meta({ id: 'ImmichQuery' });
|
||||
|
||||
export enum AssetType {
|
||||
Image = 'IMAGE',
|
||||
Video = 'VIDEO',
|
||||
@@ -56,11 +48,6 @@ export enum ChecksumAlgorithm {
|
||||
sha1Path = 'sha1-path',
|
||||
}
|
||||
|
||||
export const ChecksumAlgorithmSchema = z
|
||||
.enum(ChecksumAlgorithm)
|
||||
.describe('Checksum algorithm')
|
||||
.meta({ id: 'ChecksumAlgorithmEnum' });
|
||||
|
||||
export enum AssetFileType {
|
||||
/**
|
||||
* An full/large-size image extracted/converted from RAW photos
|
||||
@@ -72,8 +59,6 @@ export enum AssetFileType {
|
||||
EncodedVideo = 'encoded_video',
|
||||
}
|
||||
|
||||
export const AssetFileTypeSchema = z.enum(AssetFileType).describe('Asset file type').meta({ id: 'AssetFileType' });
|
||||
|
||||
export enum AlbumUserRole {
|
||||
Editor = 'editor',
|
||||
Viewer = 'viewer',
|
||||
@@ -313,8 +298,6 @@ export enum Permission {
|
||||
AdminAuthUnlinkAll = 'adminAuth.unlinkAll',
|
||||
}
|
||||
|
||||
export const PermissionSchema = z.enum(Permission).describe('Permission').meta({ id: 'Permission' });
|
||||
|
||||
export enum SharedLinkType {
|
||||
Album = 'ALBUM',
|
||||
|
||||
@@ -351,11 +334,6 @@ export enum SystemMetadataKey {
|
||||
License = 'license',
|
||||
}
|
||||
|
||||
export const SystemMetadataKeySchema = z
|
||||
.enum(SystemMetadataKey)
|
||||
.describe('System metadata key')
|
||||
.meta({ id: 'SystemMetadataKey' });
|
||||
|
||||
export enum UserMetadataKey {
|
||||
Preferences = 'preferences',
|
||||
License = 'license',
|
||||
@@ -371,11 +349,6 @@ export enum AssetMetadataKey {
|
||||
MobileApp = 'mobile-app',
|
||||
}
|
||||
|
||||
export const AssetMetadataKeySchema = z
|
||||
.enum(AssetMetadataKey)
|
||||
.describe('Asset metadata key')
|
||||
.meta({ id: 'AssetMetadataKey' });
|
||||
|
||||
export enum UserAvatarColor {
|
||||
Primary = 'primary',
|
||||
Pink = 'pink',
|
||||
@@ -408,8 +381,6 @@ export enum AssetStatus {
|
||||
Deleted = 'deleted',
|
||||
}
|
||||
|
||||
export const AssetStatusSchema = z.enum(AssetStatus).describe('Asset status').meta({ id: 'AssetStatus' });
|
||||
|
||||
export enum SourceType {
|
||||
MachineLearning = 'machine-learning',
|
||||
Exif = 'exif',
|
||||
@@ -434,20 +405,14 @@ export enum AssetPathType {
|
||||
EncodedVideo = 'encoded_video',
|
||||
}
|
||||
|
||||
export const AssetPathTypeSchema = z.enum(AssetPathType).describe('Asset path type').meta({ id: 'AssetPathType' });
|
||||
|
||||
export enum PersonPathType {
|
||||
Face = 'face',
|
||||
}
|
||||
|
||||
export const PersonPathTypeSchema = z.enum(PersonPathType).describe('Person path type').meta({ id: 'PersonPathType' });
|
||||
|
||||
export enum UserPathType {
|
||||
Profile = 'profile',
|
||||
}
|
||||
|
||||
export const UserPathTypeSchema = z.enum(UserPathType).describe('User path type').meta({ id: 'UserPathType' });
|
||||
|
||||
export type PathType = AssetFileType | AssetPathType | PersonPathType | UserPathType;
|
||||
|
||||
export enum TranscodePolicy {
|
||||
@@ -470,11 +435,6 @@ export enum TranscodeTarget {
|
||||
All = 'ALL',
|
||||
}
|
||||
|
||||
export const TranscodeTargetSchema = z
|
||||
.enum(TranscodeTarget)
|
||||
.describe('Transcode target')
|
||||
.meta({ id: 'TranscodeTarget' });
|
||||
|
||||
export enum VideoCodec {
|
||||
H264 = 'h264',
|
||||
Hevc = 'hevc',
|
||||
@@ -556,11 +516,6 @@ export enum RawExtractedFormat {
|
||||
Jxl = 'jxl',
|
||||
}
|
||||
|
||||
export const RawExtractedFormatSchema = z
|
||||
.enum(RawExtractedFormat)
|
||||
.describe('Raw extracted format')
|
||||
.meta({ id: 'RawExtractedFormat' });
|
||||
|
||||
export enum LogLevel {
|
||||
Verbose = 'verbose',
|
||||
Debug = 'debug',
|
||||
@@ -586,38 +541,25 @@ export enum ApiCustomExtension {
|
||||
State = 'x-immich-state',
|
||||
}
|
||||
|
||||
export const ApiCustomExtensionSchema = z
|
||||
.enum(ApiCustomExtension)
|
||||
.describe('API custom extension')
|
||||
.meta({ id: 'ApiCustomExtension' });
|
||||
|
||||
export enum MetadataKey {
|
||||
AuthRoute = 'auth_route',
|
||||
AdminRoute = 'admin_route',
|
||||
SharedRoute = 'shared_route',
|
||||
ApiKeySecurity = 'api_key',
|
||||
EventConfig = 'event_config',
|
||||
JobConfig = 'job_config',
|
||||
TelemetryEnabled = 'telemetry_enabled',
|
||||
}
|
||||
|
||||
export const MetadataKeySchema = z.enum(MetadataKey).describe('Metadata key').meta({ id: 'MetadataKey' });
|
||||
|
||||
export enum RouteKey {
|
||||
Asset = 'assets',
|
||||
User = 'users',
|
||||
}
|
||||
|
||||
export const RouteKeySchema = z.enum(RouteKey).describe('Route key').meta({ id: 'RouteKey' });
|
||||
|
||||
export enum CacheControl {
|
||||
PrivateWithCache = 'private_with_cache',
|
||||
PrivateWithoutCache = 'private_without_cache',
|
||||
None = 'none',
|
||||
}
|
||||
|
||||
export const CacheControlSchema = z.enum(CacheControl).describe('Cache control').meta({ id: 'CacheControl' });
|
||||
|
||||
export enum ImmichEnvironment {
|
||||
Development = 'development',
|
||||
Testing = 'testing',
|
||||
@@ -635,8 +577,6 @@ export enum ImmichWorker {
|
||||
Microservices = 'microservices',
|
||||
}
|
||||
|
||||
export const ImmichWorkerSchema = z.enum(ImmichWorker).describe('Immich worker').meta({ id: 'ImmichWorker' });
|
||||
|
||||
export enum ImmichTelemetry {
|
||||
Host = 'host',
|
||||
Api = 'api',
|
||||
@@ -645,11 +585,6 @@ export enum ImmichTelemetry {
|
||||
Job = 'job',
|
||||
}
|
||||
|
||||
export const ImmichTelemetrySchema = z
|
||||
.enum(ImmichTelemetry)
|
||||
.describe('Immich telemetry')
|
||||
.meta({ id: 'ImmichTelemetry' });
|
||||
|
||||
export enum ExifOrientation {
|
||||
Horizontal = 1,
|
||||
MirrorHorizontal = 2,
|
||||
@@ -661,11 +596,6 @@ export enum ExifOrientation {
|
||||
Rotate270CW = 8,
|
||||
}
|
||||
|
||||
export const ExifOrientationSchema = z
|
||||
.enum(ExifOrientation)
|
||||
.describe('EXIF orientation')
|
||||
.meta({ id: 'ExifOrientation' });
|
||||
|
||||
export enum DatabaseExtension {
|
||||
Cube = 'cube',
|
||||
EarthDistance = 'earthdistance',
|
||||
@@ -674,11 +604,6 @@ export enum DatabaseExtension {
|
||||
VectorChord = 'vchord',
|
||||
}
|
||||
|
||||
export const DatabaseExtensionSchema = z
|
||||
.enum(DatabaseExtension)
|
||||
.describe('Database extension')
|
||||
.meta({ id: 'DatabaseExtension' });
|
||||
|
||||
export enum BootstrapEventPriority {
|
||||
// Database service should be initialized before anything else, most other services need database access
|
||||
DatabaseService = -200,
|
||||
@@ -690,11 +615,6 @@ export enum BootstrapEventPriority {
|
||||
SystemConfig = 100,
|
||||
}
|
||||
|
||||
export const BootstrapEventPrioritySchema = z
|
||||
.enum(BootstrapEventPriority)
|
||||
.describe('Bootstrap event priority')
|
||||
.meta({ id: 'BootstrapEventPriority' });
|
||||
|
||||
export enum QueueName {
|
||||
ThumbnailGeneration = 'thumbnailGeneration',
|
||||
MetadataExtraction = 'metadataExtraction',
|
||||
@@ -833,21 +753,15 @@ export enum JobStatus {
|
||||
Skipped = 'skipped',
|
||||
}
|
||||
|
||||
export const JobStatusSchema = z.enum(JobStatus).describe('Job status').meta({ id: 'JobStatus' });
|
||||
|
||||
export enum QueueCleanType {
|
||||
Failed = 'failed',
|
||||
}
|
||||
|
||||
export const QueueCleanTypeSchema = z.enum(QueueCleanType).describe('Queue clean type').meta({ id: 'QueueCleanType' });
|
||||
|
||||
export enum VectorIndex {
|
||||
Clip = 'clip_index',
|
||||
Face = 'face_index',
|
||||
}
|
||||
|
||||
export const VectorIndexSchema = z.enum(VectorIndex).describe('Vector index').meta({ id: 'VectorIndex' });
|
||||
|
||||
export enum DatabaseLock {
|
||||
GeodataImport = 100,
|
||||
Migrations = 200,
|
||||
@@ -865,8 +779,6 @@ export enum DatabaseLock {
|
||||
VersionCheck = 800,
|
||||
}
|
||||
|
||||
export const DatabaseLockSchema = z.enum(DatabaseLock).describe('Database lock').meta({ id: 'DatabaseLock' });
|
||||
|
||||
export enum MaintenanceAction {
|
||||
Start = 'start',
|
||||
End = 'end',
|
||||
@@ -883,8 +795,6 @@ export enum ExitCode {
|
||||
AppRestart = 7,
|
||||
}
|
||||
|
||||
export const ExitCodeSchema = z.enum(ExitCode).describe('Exit code').meta({ id: 'ExitCode' });
|
||||
|
||||
export enum SyncRequestType {
|
||||
AlbumsV1 = 'AlbumsV1',
|
||||
AlbumUsersV1 = 'AlbumUsersV1',
|
||||
@@ -1043,8 +953,6 @@ export enum CronJob {
|
||||
VersionCheck = 'VersionCheck',
|
||||
}
|
||||
|
||||
export const CronJobSchema = z.enum(CronJob).describe('Cron job').meta({ id: 'CronJob' });
|
||||
|
||||
export enum ApiTag {
|
||||
Activities = 'Activities',
|
||||
Albums = 'Albums',
|
||||
@@ -1085,8 +993,6 @@ export enum ApiTag {
|
||||
Workflows = 'Workflows',
|
||||
}
|
||||
|
||||
export const ApiTagSchema = z.enum(ApiTag).describe('API tag').meta({ id: 'ApiTag' });
|
||||
|
||||
export enum PluginContext {
|
||||
Asset = 'asset',
|
||||
Album = 'album',
|
||||
|
||||
@@ -33,12 +33,6 @@ export interface ReverseGeocodeResult {
|
||||
city: string | null;
|
||||
}
|
||||
|
||||
export interface MapMarker extends ReverseGeocodeResult {
|
||||
id: string;
|
||||
lat: number;
|
||||
lon: number;
|
||||
}
|
||||
|
||||
interface MapDB extends DB {
|
||||
geodata_places_tmp: GeodataPlacesTable;
|
||||
naturalearth_countries_tmp: NaturalEarthCountriesTable;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -65,13 +65,8 @@ export interface DecodeToBufferOptions extends DecodeImageOptions {
|
||||
}
|
||||
|
||||
export type GenerateThumbnailOptions = Pick<ImageOptions, 'format' | 'quality' | 'progressive'> & DecodeToBufferOptions;
|
||||
|
||||
export type GenerateThumbnailFromBufferOptions = GenerateThumbnailOptions & { raw: RawImageInfo };
|
||||
|
||||
export type GenerateThumbhashOptions = DecodeImageOptions;
|
||||
|
||||
export type GenerateThumbhashFromBufferOptions = GenerateThumbhashOptions & { raw: RawImageInfo };
|
||||
|
||||
export interface GenerateThumbnailsOptions {
|
||||
colorspace: string;
|
||||
preview?: ImageOptions;
|
||||
|
||||
@@ -10,8 +10,6 @@ import { SystemMetadataRepository } from 'src/repositories/system-metadata.repos
|
||||
import { DeepPartial } from 'src/types';
|
||||
import { getKeysDeep, unsetDeep } from 'src/utils/misc';
|
||||
|
||||
export type SystemConfigValidator = (config: SystemConfig, newConfig: SystemConfig) => void | Promise<void>;
|
||||
|
||||
type RepoDeps = {
|
||||
configRepo: ConfigRepository;
|
||||
metadataRepo: SystemMetadataRepository;
|
||||
|
||||
@@ -1,60 +1,11 @@
|
||||
import { createAdapter } from '@socket.io/redis-adapter';
|
||||
import Redis from 'ioredis';
|
||||
import { SignJWT } from 'jose';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { join } from 'node:path';
|
||||
import { Server as SocketIO } from 'socket.io';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { MaintenanceAuthDto, MaintenanceDetectInstallResponseDto } from 'src/dtos/maintenance.dto';
|
||||
import { StorageFolder } from 'src/enum';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { AppRestartEvent } from 'src/repositories/event.repository';
|
||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||
|
||||
export function sendOneShotAppRestart(state: AppRestartEvent): void {
|
||||
const server = new SocketIO();
|
||||
const { redis } = new ConfigRepository().getEnv();
|
||||
const pubClient = new Redis(redis);
|
||||
const subClient = pubClient.duplicate();
|
||||
server.adapter(createAdapter(pubClient, subClient));
|
||||
|
||||
/**
|
||||
* Keep trying until we manage to stop Immich
|
||||
*
|
||||
* Sometimes there appear to be communication
|
||||
* issues between to the other servers.
|
||||
*
|
||||
* This issue only occurs with this method.
|
||||
*/
|
||||
async function tryTerminate() {
|
||||
while (true) {
|
||||
try {
|
||||
const responses = await server.serverSideEmitWithAck('AppRestart', state);
|
||||
if (responses.length > 0) {
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error('Encountered an error while telling Immich to stop.');
|
||||
}
|
||||
|
||||
console.info(
|
||||
"\nIt doesn't appear that Immich stopped, trying again in a moment.\nIf Immich is already not running, you can ignore this error.",
|
||||
);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 1e3));
|
||||
}
|
||||
}
|
||||
|
||||
// => corresponds to notification.service.ts#onAppRestart
|
||||
server.emit('AppRestartV1', state, () => {
|
||||
void tryTerminate().finally(() => {
|
||||
pubClient.disconnect();
|
||||
subClient.disconnect();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function createMaintenanceLoginUrl(
|
||||
baseUrl: string,
|
||||
auth: MaintenanceAuthDto,
|
||||
|
||||
+120
-85
@@ -91,14 +91,14 @@ export class BaseConfig implements VideoCodecSWConfig {
|
||||
) {
|
||||
const options = {
|
||||
inputOptions: this.getBaseInputOptions(videoStream, format),
|
||||
outputOptions: [...this.getBaseOutputOptions(target, videoStream, audioStream), '-v verbose'],
|
||||
outputOptions: [...this.getBaseOutputOptions(target, videoStream, audioStream), '-v', 'verbose'],
|
||||
twoPass: this.eligibleForTwoPass(),
|
||||
progress: { frameCount: videoStream.frameCount, percentInterval: 5 },
|
||||
} as TranscodeCommand;
|
||||
if ([TranscodeTarget.All, TranscodeTarget.Video].includes(target)) {
|
||||
const filters = this.getFilterOptions(videoStream);
|
||||
if (filters.length > 0) {
|
||||
options.outputOptions.push(`-vf ${filters.join(',')}`);
|
||||
options.outputOptions.push('-vf', filters.join(','));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,36 +121,40 @@ export class BaseConfig implements VideoCodecSWConfig {
|
||||
const audioCodec = [TranscodeTarget.All, TranscodeTarget.Audio].includes(target) ? this.getAudioEncoder() : 'copy';
|
||||
|
||||
const options = [
|
||||
`-c:v ${videoCodec}`,
|
||||
`-c:a ${audioCodec}`,
|
||||
'-c:v',
|
||||
videoCodec,
|
||||
'-c:a',
|
||||
audioCodec,
|
||||
// Makes a second pass moving the moov atom to the
|
||||
// beginning of the file for improved playback speed.
|
||||
'-movflags faststart',
|
||||
'-fps_mode passthrough',
|
||||
// explicitly selects the video stream instead of leaving it up to FFmpeg
|
||||
`-map 0:${videoStream.index}`,
|
||||
// Strip metadata like capture date, camera, and GPS
|
||||
'-map_metadata -1',
|
||||
'-movflags',
|
||||
'faststart',
|
||||
'-fps_mode',
|
||||
'passthrough',
|
||||
'-map',
|
||||
`0:${videoStream.index}`,
|
||||
'-map_metadata',
|
||||
'-1',
|
||||
];
|
||||
|
||||
if (audioStream) {
|
||||
options.push(`-map 0:${audioStream.index}`);
|
||||
options.push('-map', `0:${audioStream.index}`);
|
||||
}
|
||||
if (this.getBFrames() > -1) {
|
||||
options.push(`-bf ${this.getBFrames()}`);
|
||||
options.push('-bf', `${this.getBFrames()}`);
|
||||
}
|
||||
if (this.getRefs() > 0) {
|
||||
options.push(`-refs ${this.getRefs()}`);
|
||||
options.push('-refs', `${this.getRefs()}`);
|
||||
}
|
||||
if (this.getGopSize() > 0) {
|
||||
options.push(`-g ${this.getGopSize()}`);
|
||||
options.push('-g', `${this.getGopSize()}`);
|
||||
}
|
||||
|
||||
if (
|
||||
this.config.targetVideoCodec === VideoCodec.Hevc &&
|
||||
(videoCodec !== 'copy' || videoStream.codecName === 'hevc')
|
||||
) {
|
||||
options.push('-tag:v hvc1');
|
||||
options.push('-tag:v', 'hvc1');
|
||||
}
|
||||
|
||||
return options;
|
||||
@@ -173,26 +177,32 @@ export class BaseConfig implements VideoCodecSWConfig {
|
||||
}
|
||||
|
||||
getPresetOptions() {
|
||||
return [`-preset ${this.config.preset}`];
|
||||
return ['-preset', this.config.preset];
|
||||
}
|
||||
|
||||
getBitrateOptions() {
|
||||
const bitrates = this.getBitrateDistribution();
|
||||
if (this.eligibleForTwoPass()) {
|
||||
return [
|
||||
`-b:v ${bitrates.target}${bitrates.unit}`,
|
||||
`-minrate ${bitrates.min}${bitrates.unit}`,
|
||||
`-maxrate ${bitrates.max}${bitrates.unit}`,
|
||||
'-b:v',
|
||||
`${bitrates.target}${bitrates.unit}`,
|
||||
'-minrate',
|
||||
`${bitrates.min}${bitrates.unit}`,
|
||||
'-maxrate',
|
||||
`${bitrates.max}${bitrates.unit}`,
|
||||
];
|
||||
} else if (bitrates.max > 0) {
|
||||
// -bufsize is the peak possible bitrate at any moment, while -maxrate is the max rolling average bitrate
|
||||
return [
|
||||
`-${this.useCQP() ? 'q:v' : 'crf'} ${this.config.crf}`,
|
||||
`-maxrate ${bitrates.max}${bitrates.unit}`,
|
||||
`-bufsize ${bitrates.max * 2}${bitrates.unit}`,
|
||||
`-${this.useCQP() ? 'q:v' : 'crf'}`,
|
||||
`${this.config.crf}`,
|
||||
'-maxrate',
|
||||
`${bitrates.max}${bitrates.unit}`,
|
||||
'-bufsize',
|
||||
`${bitrates.max * 2}${bitrates.unit}`,
|
||||
];
|
||||
} else {
|
||||
return [`-${this.useCQP() ? 'q:v' : 'crf'} ${this.config.crf}`];
|
||||
return [`-${this.useCQP() ? 'q:v' : 'crf'}`, `${this.config.crf}`];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,7 +214,7 @@ export class BaseConfig implements VideoCodecSWConfig {
|
||||
if (this.config.threads <= 0) {
|
||||
return [];
|
||||
}
|
||||
return [`-threads ${this.config.threads}`];
|
||||
return ['-threads', `${this.config.threads}`];
|
||||
}
|
||||
|
||||
eligibleForTwoPass() {
|
||||
@@ -395,8 +405,8 @@ export class ThumbnailConfig extends BaseConfig {
|
||||
// skip_frame nointra skips all frames for some MPEG-TS files. Look at ffmpeg tickets 7950 and 7895 for more details.
|
||||
const options =
|
||||
format?.formatName === 'mpegts'
|
||||
? ['-sws_flags accurate_rnd+full_chroma_int']
|
||||
: ['-skip_frame nointra', '-sws_flags accurate_rnd+full_chroma_int'];
|
||||
? ['-sws_flags', 'accurate_rnd+full_chroma_int']
|
||||
: ['-skip_frame', 'nointra', '-sws_flags', 'accurate_rnd+full_chroma_int'];
|
||||
|
||||
const metadataOverrides = [];
|
||||
if (videoStream.colorPrimaries === 'reserved') {
|
||||
@@ -413,14 +423,14 @@ export class ThumbnailConfig extends BaseConfig {
|
||||
|
||||
if (metadataOverrides.length > 0) {
|
||||
// workaround for https://fftrac-bg.ffmpeg.org/ticket/11020
|
||||
options.push(`-bsf:v ${videoStream.codecName}_metadata=${metadataOverrides.join(':')}`);
|
||||
options.push('-bsf:v', `${videoStream.codecName}_metadata=${metadataOverrides.join(':')}`);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
getBaseOutputOptions() {
|
||||
return ['-fps_mode vfr', '-frames:v 1', '-update 1'];
|
||||
return ['-fps_mode', 'vfr', '-frames:v', '1', '-update', '1'];
|
||||
}
|
||||
|
||||
getFilterOptions(videoStream: VideoStreamInfo): string[] {
|
||||
@@ -455,7 +465,7 @@ export class H264Config extends BaseConfig {
|
||||
getOutputThreadOptions() {
|
||||
const options = super.getOutputThreadOptions();
|
||||
if (this.config.threads === 1) {
|
||||
options.push('-x264-params frame-threads=1:pools=none');
|
||||
options.push('-x264-params', 'frame-threads=1:pools=none');
|
||||
}
|
||||
|
||||
return options;
|
||||
@@ -466,7 +476,7 @@ export class HEVCConfig extends BaseConfig {
|
||||
getOutputThreadOptions() {
|
||||
const options = super.getOutputThreadOptions();
|
||||
if (this.config.threads === 1) {
|
||||
options.push('-x265-params frame-threads=1:pools=none');
|
||||
options.push('-x265-params', 'frame-threads=1:pools=none');
|
||||
}
|
||||
|
||||
return options;
|
||||
@@ -477,7 +487,7 @@ export class VP9Config extends BaseConfig {
|
||||
getPresetOptions() {
|
||||
const speed = Math.min(this.getPresetIndex(), 5); // values over 5 require realtime mode, which is its own can of worms since it overrides -crf and -threads
|
||||
if (speed >= 0) {
|
||||
return [`-cpu-used ${speed}`];
|
||||
return ['-cpu-used', `${speed}`];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
@@ -486,17 +496,20 @@ export class VP9Config extends BaseConfig {
|
||||
const bitrates = this.getBitrateDistribution();
|
||||
if (bitrates.max > 0 && this.eligibleForTwoPass()) {
|
||||
return [
|
||||
`-b:v ${bitrates.target}${bitrates.unit}`,
|
||||
`-minrate ${bitrates.min}${bitrates.unit}`,
|
||||
`-maxrate ${bitrates.max}${bitrates.unit}`,
|
||||
'-b:v',
|
||||
`${bitrates.target}${bitrates.unit}`,
|
||||
'-minrate',
|
||||
`${bitrates.min}${bitrates.unit}`,
|
||||
'-maxrate',
|
||||
`${bitrates.max}${bitrates.unit}`,
|
||||
];
|
||||
}
|
||||
|
||||
return [`-${this.useCQP() ? 'q:v' : 'crf'} ${this.config.crf}`, `-b:v ${bitrates.max}${bitrates.unit}`];
|
||||
return [`-${this.useCQP() ? 'q:v' : 'crf'}`, `${this.config.crf}`, '-b:v', `${bitrates.max}${bitrates.unit}`];
|
||||
}
|
||||
|
||||
getOutputThreadOptions() {
|
||||
return ['-row-mt 1', ...super.getOutputThreadOptions()];
|
||||
return ['-row-mt', '1', ...super.getOutputThreadOptions()];
|
||||
}
|
||||
|
||||
eligibleForTwoPass() {
|
||||
@@ -512,13 +525,13 @@ export class AV1Config extends BaseConfig {
|
||||
getPresetOptions() {
|
||||
const speed = this.getPresetIndex() + 4; // Use 4 as slowest, giving us an effective range of 4-12 which is far more useful than 0-8
|
||||
if (speed >= 0) {
|
||||
return [`-preset ${speed}`];
|
||||
return ['-preset', `${speed}`];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
getBitrateOptions() {
|
||||
const options = [`-crf ${this.config.crf}`];
|
||||
const options = ['-crf', `${this.config.crf}`];
|
||||
const bitrates = this.getBitrateDistribution();
|
||||
const svtparams = [];
|
||||
if (this.config.threads > 0) {
|
||||
@@ -528,7 +541,7 @@ export class AV1Config extends BaseConfig {
|
||||
svtparams.push(`mbr=${bitrates.max}${bitrates.unit}`);
|
||||
}
|
||||
if (svtparams.length > 0) {
|
||||
options.push(`-svtav1-params ${svtparams.join(':')}`);
|
||||
options.push('-svtav1-params', svtparams.join(':'));
|
||||
}
|
||||
return options;
|
||||
}
|
||||
@@ -552,23 +565,27 @@ export class NvencSwDecodeConfig extends BaseHWConfig {
|
||||
}
|
||||
|
||||
getBaseInputOptions() {
|
||||
return [`-init_hw_device cuda=cuda:${this.device}`, '-filter_hw_device cuda'];
|
||||
return ['-init_hw_device', `cuda=cuda:${this.device}`, '-filter_hw_device', 'cuda'];
|
||||
}
|
||||
|
||||
getBaseOutputOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) {
|
||||
const options = [
|
||||
// below settings recommended from https://docs.nvidia.com/video-technologies/video-codec-sdk/12.0/ffmpeg-with-nvidia-gpu/index.html#command-line-for-latency-tolerant-high-quality-transcoding
|
||||
'-tune hq',
|
||||
'-qmin 0',
|
||||
'-rc-lookahead 20',
|
||||
'-i_qfactor 0.75',
|
||||
'-tune',
|
||||
'hq',
|
||||
'-qmin',
|
||||
'0',
|
||||
'-rc-lookahead',
|
||||
'20',
|
||||
'-i_qfactor',
|
||||
'0.75',
|
||||
...super.getBaseOutputOptions(target, videoStream, audioStream),
|
||||
];
|
||||
if (this.getBFrames() > 0) {
|
||||
options.push('-b_ref_mode middle', '-b_qfactor 1.1');
|
||||
options.push('-b_ref_mode', 'middle', '-b_qfactor', '1.1');
|
||||
}
|
||||
if (this.config.temporalAQ) {
|
||||
options.push('-temporal-aq 1');
|
||||
options.push('-temporal-aq', '1');
|
||||
}
|
||||
return options;
|
||||
}
|
||||
@@ -589,26 +606,33 @@ export class NvencSwDecodeConfig extends BaseHWConfig {
|
||||
return [];
|
||||
}
|
||||
presetIndex = 7 - Math.min(6, presetIndex); // map to p1-p7; p7 is the highest quality, so reverse index
|
||||
return [`-preset p${presetIndex}`];
|
||||
return ['-preset', `p${presetIndex}`];
|
||||
}
|
||||
|
||||
getBitrateOptions() {
|
||||
const bitrates = this.getBitrateDistribution();
|
||||
if (bitrates.max > 0 && this.config.twoPass) {
|
||||
return [
|
||||
`-b:v ${bitrates.target}${bitrates.unit}`,
|
||||
`-maxrate ${bitrates.max}${bitrates.unit}`,
|
||||
`-bufsize ${bitrates.target}${bitrates.unit}`,
|
||||
'-multipass 2',
|
||||
'-b:v',
|
||||
`${bitrates.target}${bitrates.unit}`,
|
||||
'-maxrate',
|
||||
`${bitrates.max}${bitrates.unit}`,
|
||||
'-bufsize',
|
||||
`${bitrates.target}${bitrates.unit}`,
|
||||
'-multipass',
|
||||
'2',
|
||||
];
|
||||
} else if (bitrates.max > 0) {
|
||||
return [
|
||||
`-cq:v ${this.config.crf}`,
|
||||
`-maxrate ${bitrates.max}${bitrates.unit}`,
|
||||
`-bufsize ${bitrates.target}${bitrates.unit}`,
|
||||
'-cq:v',
|
||||
`${this.config.crf}`,
|
||||
'-maxrate',
|
||||
`${bitrates.max}${bitrates.unit}`,
|
||||
'-bufsize',
|
||||
`${bitrates.target}${bitrates.unit}`,
|
||||
];
|
||||
} else {
|
||||
return [`-cq:v ${this.config.crf}`];
|
||||
return ['-cq:v', `${this.config.crf}`];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,7 +651,7 @@ export class NvencSwDecodeConfig extends BaseHWConfig {
|
||||
|
||||
export class NvencHwDecodeConfig extends NvencSwDecodeConfig {
|
||||
getBaseInputOptions() {
|
||||
return ['-hwaccel cuda', '-hwaccel_output_format cuda', '-noautorotate', ...this.getInputThreadOptions()];
|
||||
return ['-hwaccel', 'cuda', '-hwaccel_output_format', 'cuda', '-noautorotate', ...this.getInputThreadOptions()];
|
||||
}
|
||||
|
||||
getFilterOptions(videoStream: VideoStreamInfo) {
|
||||
@@ -664,7 +688,7 @@ export class NvencHwDecodeConfig extends NvencSwDecodeConfig {
|
||||
}
|
||||
|
||||
getInputThreadOptions() {
|
||||
return [`-threads 1`];
|
||||
return ['-threads', '1'];
|
||||
}
|
||||
|
||||
getOutputThreadOptions() {
|
||||
@@ -674,14 +698,14 @@ export class NvencHwDecodeConfig extends NvencSwDecodeConfig {
|
||||
|
||||
export class QsvSwDecodeConfig extends BaseHWConfig {
|
||||
getBaseInputOptions() {
|
||||
return [`-init_hw_device qsv=hw,child_device=${this.device}`, '-filter_hw_device hw'];
|
||||
return ['-init_hw_device', `qsv=hw,child_device=${this.device}`, '-filter_hw_device', 'hw'];
|
||||
}
|
||||
|
||||
getBaseOutputOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) {
|
||||
const options = super.getBaseOutputOptions(target, videoStream, audioStream);
|
||||
// VP9 requires enabling low power mode https://git.ffmpeg.org/gitweb/ffmpeg.git/commit/33583803e107b6d532def0f9d949364b01b6ad5a
|
||||
if (this.config.targetVideoCodec === VideoCodec.Vp9) {
|
||||
options.push('-low_power 1');
|
||||
options.push('-low_power', '1');
|
||||
}
|
||||
return options;
|
||||
}
|
||||
@@ -701,14 +725,14 @@ export class QsvSwDecodeConfig extends BaseHWConfig {
|
||||
return [];
|
||||
}
|
||||
presetIndex = Math.min(6, presetIndex) + 1; // 1 to 7
|
||||
return [`-preset ${presetIndex}`];
|
||||
return ['-preset', `${presetIndex}`];
|
||||
}
|
||||
|
||||
getBitrateOptions() {
|
||||
const options = [`-${this.useCQP() ? 'q:v' : 'global_quality:v'} ${this.config.crf}`];
|
||||
const options = [`-${this.useCQP() ? 'q:v' : 'global_quality:v'}`, `${this.config.crf}`];
|
||||
const bitrates = this.getBitrateDistribution();
|
||||
if (bitrates.max > 0) {
|
||||
options.push(`-maxrate ${bitrates.max}${bitrates.unit}`, `-bufsize ${bitrates.max * 2}${bitrates.unit}`);
|
||||
options.push('-maxrate', `${bitrates.max}${bitrates.unit}`, '-bufsize', `${bitrates.max * 2}${bitrates.unit}`);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
@@ -744,11 +768,15 @@ export class QsvSwDecodeConfig extends BaseHWConfig {
|
||||
export class QsvHwDecodeConfig extends QsvSwDecodeConfig {
|
||||
getBaseInputOptions() {
|
||||
return [
|
||||
'-hwaccel qsv',
|
||||
'-hwaccel_output_format qsv',
|
||||
'-async_depth 4',
|
||||
'-hwaccel',
|
||||
'qsv',
|
||||
'-hwaccel_output_format',
|
||||
'qsv',
|
||||
'-async_depth',
|
||||
'4',
|
||||
'-noautorotate',
|
||||
`-qsv_device ${this.device}`,
|
||||
'-qsv_device',
|
||||
this.device,
|
||||
...this.getInputThreadOptions(),
|
||||
];
|
||||
}
|
||||
@@ -791,13 +819,13 @@ export class QsvHwDecodeConfig extends QsvSwDecodeConfig {
|
||||
}
|
||||
|
||||
getInputThreadOptions() {
|
||||
return [`-threads 1`];
|
||||
return ['-threads', '1'];
|
||||
}
|
||||
}
|
||||
|
||||
export class VaapiSwDecodeConfig extends BaseHWConfig {
|
||||
getBaseInputOptions() {
|
||||
return [`-init_hw_device vaapi=accel:${this.device}`, '-filter_hw_device accel'];
|
||||
return ['-init_hw_device', `vaapi=accel:${this.device}`, '-filter_hw_device', 'accel'];
|
||||
}
|
||||
|
||||
getFilterOptions(videoStream: VideoStreamInfo) {
|
||||
@@ -816,7 +844,7 @@ export class VaapiSwDecodeConfig extends BaseHWConfig {
|
||||
return [];
|
||||
}
|
||||
presetIndex = Math.min(6, presetIndex) + 1; // 1 to 7
|
||||
return [`-compression_level ${presetIndex}`];
|
||||
return ['-compression_level', `${presetIndex}`];
|
||||
}
|
||||
|
||||
getBitrateOptions() {
|
||||
@@ -824,21 +852,25 @@ export class VaapiSwDecodeConfig extends BaseHWConfig {
|
||||
const options = [];
|
||||
|
||||
if (this.config.targetVideoCodec === VideoCodec.Vp9) {
|
||||
options.push('-bsf:v vp9_raw_reorder,vp9_superframe');
|
||||
options.push('-bsf:v', 'vp9_raw_reorder,vp9_superframe');
|
||||
}
|
||||
|
||||
// VAAPI doesn't allow setting both quality and max bitrate
|
||||
if (bitrates.max > 0) {
|
||||
options.push(
|
||||
`-b:v ${bitrates.target}${bitrates.unit}`,
|
||||
`-maxrate ${bitrates.max}${bitrates.unit}`,
|
||||
`-minrate ${bitrates.min}${bitrates.unit}`,
|
||||
'-rc_mode 3',
|
||||
'-b:v',
|
||||
`${bitrates.target}${bitrates.unit}`,
|
||||
'-maxrate',
|
||||
`${bitrates.max}${bitrates.unit}`,
|
||||
'-minrate',
|
||||
`${bitrates.min}${bitrates.unit}`,
|
||||
'-rc_mode',
|
||||
'3',
|
||||
); // variable bitrate
|
||||
} else if (this.useCQP()) {
|
||||
options.push(`-qp:v ${this.config.crf}`, `-global_quality:v ${this.config.crf}`, '-rc_mode 1');
|
||||
options.push('-qp:v', `${this.config.crf}`, '-global_quality:v', `${this.config.crf}`, '-rc_mode', '1');
|
||||
} else {
|
||||
options.push(`-global_quality:v ${this.config.crf}`, '-rc_mode 4');
|
||||
options.push('-global_quality:v', `${this.config.crf}`, '-rc_mode', '4');
|
||||
}
|
||||
|
||||
return options;
|
||||
@@ -856,10 +888,13 @@ export class VaapiSwDecodeConfig extends BaseHWConfig {
|
||||
export class VaapiHwDecodeConfig extends VaapiSwDecodeConfig {
|
||||
getBaseInputOptions() {
|
||||
return [
|
||||
'-hwaccel vaapi',
|
||||
'-hwaccel_output_format vaapi',
|
||||
'-hwaccel',
|
||||
'vaapi',
|
||||
'-hwaccel_output_format',
|
||||
'vaapi',
|
||||
'-noautorotate',
|
||||
`-hwaccel_device ${this.device}`,
|
||||
'-hwaccel_device',
|
||||
this.device,
|
||||
...this.getInputThreadOptions(),
|
||||
];
|
||||
}
|
||||
@@ -902,7 +937,7 @@ export class VaapiHwDecodeConfig extends VaapiSwDecodeConfig {
|
||||
}
|
||||
|
||||
getInputThreadOptions() {
|
||||
return [`-threads 1`];
|
||||
return ['-threads', '1'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -919,11 +954,11 @@ export class RkmppSwDecodeConfig extends BaseHWConfig {
|
||||
switch (this.config.targetVideoCodec) {
|
||||
case VideoCodec.H264: {
|
||||
// from ffmpeg_mpp help, commonly referred to as H264 level 5.1
|
||||
return ['-level 51'];
|
||||
return ['-level', '51'];
|
||||
}
|
||||
case VideoCodec.Hevc: {
|
||||
// from ffmpeg_mpp help, commonly referred to as HEVC level 5.1
|
||||
return ['-level 153'];
|
||||
return ['-level', '153'];
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Incompatible video codec for RKMPP: ${this.config.targetVideoCodec}`);
|
||||
@@ -935,10 +970,10 @@ export class RkmppSwDecodeConfig extends BaseHWConfig {
|
||||
const bitrate = this.getMaxBitrateValue();
|
||||
if (bitrate > 0) {
|
||||
// -b:v specifies max bitrate, average bitrate is derived automatically...
|
||||
return ['-rc_mode AVBR', `-b:v ${bitrate}${this.getBitrateUnit()}`];
|
||||
return ['-rc_mode', 'AVBR', '-b:v', `${bitrate}${this.getBitrateUnit()}`];
|
||||
}
|
||||
// use CRF value as QP value
|
||||
return ['-rc_mode CQP', `-qp_init ${this.config.crf}`];
|
||||
return ['-rc_mode', 'CQP', '-qp_init', `${this.config.crf}`];
|
||||
}
|
||||
|
||||
getSupportedCodecs() {
|
||||
@@ -952,7 +987,7 @@ export class RkmppSwDecodeConfig extends BaseHWConfig {
|
||||
|
||||
export class RkmppHwDecodeConfig extends RkmppSwDecodeConfig {
|
||||
getBaseInputOptions() {
|
||||
return ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga', '-noautorotate'];
|
||||
return ['-hwaccel', 'rkmpp', '-hwaccel_output_format', 'drm_prime', '-afbc', 'rga', '-noautorotate'];
|
||||
}
|
||||
|
||||
getFilterOptions(videoStream: VideoStreamInfo) {
|
||||
|
||||
-52
@@ -1,5 +1,4 @@
|
||||
import { MapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { SharedLinkResponseDto } from 'src/dtos/shared-link.dto';
|
||||
import { SharedLinkType } from 'src/enum';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
@@ -70,23 +69,6 @@ export const sharedLinkStub = {
|
||||
album: null,
|
||||
slug: null,
|
||||
}),
|
||||
readonlyNoExif: Object.freeze({
|
||||
id: '123',
|
||||
userId: authStub.admin.user.id,
|
||||
key: sharedLinkBytes,
|
||||
type: SharedLinkType.Individual,
|
||||
createdAt: today,
|
||||
expiresAt: tomorrow,
|
||||
allowUpload: false,
|
||||
allowDownload: false,
|
||||
showExif: false,
|
||||
description: null,
|
||||
password: null,
|
||||
assets: [],
|
||||
albumId: null,
|
||||
album: null,
|
||||
slug: null,
|
||||
}),
|
||||
passwordRequired: Object.freeze({
|
||||
id: '123',
|
||||
userId: authStub.admin.user.id,
|
||||
@@ -105,37 +87,3 @@ export const sharedLinkStub = {
|
||||
album: null,
|
||||
}),
|
||||
};
|
||||
|
||||
export const sharedLinkResponseStub = {
|
||||
valid: Object.freeze<SharedLinkResponseDto>({
|
||||
allowDownload: true,
|
||||
allowUpload: true,
|
||||
assets: [],
|
||||
createdAt: today,
|
||||
description: null,
|
||||
password: null,
|
||||
expiresAt: tomorrow,
|
||||
id: '123',
|
||||
key: sharedLinkBytes.toString('base64url'),
|
||||
showMetadata: true,
|
||||
type: SharedLinkType.Album,
|
||||
userId: 'admin_id',
|
||||
slug: null,
|
||||
}),
|
||||
expired: Object.freeze<SharedLinkResponseDto>({
|
||||
album: undefined,
|
||||
allowDownload: true,
|
||||
allowUpload: true,
|
||||
assets: [],
|
||||
createdAt: today,
|
||||
description: null,
|
||||
password: null,
|
||||
expiresAt: yesterday,
|
||||
id: '123',
|
||||
key: sharedLinkBytes.toString('base64url'),
|
||||
showMetadata: true,
|
||||
type: SharedLinkType.Album,
|
||||
userId: 'admin_id',
|
||||
slug: null,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -67,48 +67,3 @@ export const errorDto = {
|
||||
correlationId: expect.any(String),
|
||||
},
|
||||
};
|
||||
|
||||
export const signupResponseDto = {
|
||||
admin: {
|
||||
avatarColor: expect.any(String),
|
||||
id: expect.any(String),
|
||||
name: 'Immich Admin',
|
||||
email: 'admin@immich.cloud',
|
||||
storageLabel: 'admin',
|
||||
profileImagePath: '',
|
||||
// why? lol
|
||||
shouldChangePassword: true,
|
||||
isAdmin: true,
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
deletedAt: null,
|
||||
oauthId: '',
|
||||
quotaUsageInBytes: 0,
|
||||
quotaSizeInBytes: null,
|
||||
status: 'active',
|
||||
license: null,
|
||||
profileChangedAt: expect.any(String),
|
||||
},
|
||||
};
|
||||
|
||||
export const loginResponseDto = {
|
||||
admin: {
|
||||
accessToken: expect.any(String),
|
||||
name: 'Immich Admin',
|
||||
isAdmin: true,
|
||||
profileImagePath: '',
|
||||
shouldChangePassword: true,
|
||||
userEmail: 'admin@immich.cloud',
|
||||
userId: expect.any(String),
|
||||
},
|
||||
};
|
||||
export const deviceDto = {
|
||||
current: {
|
||||
id: expect.any(String),
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
current: true,
|
||||
deviceOS: '',
|
||||
deviceType: '',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -531,43 +531,6 @@ export const mockDuplex =
|
||||
return duplex;
|
||||
};
|
||||
|
||||
export const mockFork = vitest.fn((exitCode: number, stdout: string, stderr: string, error?: unknown) => {
|
||||
const stdoutStream = new Readable({
|
||||
read() {
|
||||
this.push(stdout); // write mock data to stdout
|
||||
this.push(null); // end stream
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
stdout: stdoutStream,
|
||||
stderr: new Readable({
|
||||
read() {
|
||||
this.push(stderr); // write mock data to stderr
|
||||
this.push(null); // end stream
|
||||
},
|
||||
}),
|
||||
stdin: new Writable({
|
||||
write(chunk, encoding, callback) {
|
||||
callback();
|
||||
},
|
||||
}),
|
||||
exitCode,
|
||||
on: vitest.fn((event, callback: any) => {
|
||||
if (event === 'close') {
|
||||
stdoutStream.once('end', () => callback(0));
|
||||
}
|
||||
if (event === 'error' && error) {
|
||||
stdoutStream.once('end', () => callback(error));
|
||||
}
|
||||
if (event === 'exit') {
|
||||
stdoutStream.once('end', () => callback(exitCode));
|
||||
}
|
||||
}),
|
||||
kill: vitest.fn(),
|
||||
} as unknown as ChildProcessWithoutNullStreams;
|
||||
});
|
||||
|
||||
export async function* makeStream<T>(items: T[] = []): AsyncIterableIterator<T> {
|
||||
for (const item of items) {
|
||||
await Promise.resolve();
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"rollup-plugin-visualizer": "^7.0.0",
|
||||
"svelte": "5.55.1",
|
||||
"svelte-check": "^4.1.5",
|
||||
"svelte-check": "^4.4.6",
|
||||
"svelte-eslint-parser": "^1.3.3",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^6.0.0",
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
mdiPlus,
|
||||
} from '@mdi/js';
|
||||
import { DateTime } from 'luxon';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { slide } from 'svelte/transition';
|
||||
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
||||
@@ -52,7 +53,6 @@
|
||||
|
||||
let { asset, currentAlbum = null }: Props = $props();
|
||||
|
||||
let showEditFaces = $derived(assetViewerManager.isEditFacesPanelOpen);
|
||||
let isOwner = $derived(authManager.authenticated && authManager.user.id === asset.ownerId);
|
||||
let people = $derived(asset.people || []);
|
||||
let unassignedFaces = $derived(asset.unassignedFaces || []);
|
||||
@@ -118,379 +118,385 @@
|
||||
// Remove the last part of the path to get the parent path
|
||||
return Route.folders({ path: getParentPath(asset.originalPath) });
|
||||
};
|
||||
|
||||
onDestroy(() => {
|
||||
assetViewerManager.closeEditFacesPanel();
|
||||
});
|
||||
</script>
|
||||
|
||||
<OnEvents onAlbumAddAssets={() => (albums = refreshAlbums())} />
|
||||
|
||||
<section class="relative p-2">
|
||||
<div class="flex place-items-center gap-2">
|
||||
<IconButton
|
||||
icon={mdiClose}
|
||||
aria-label={$t('close')}
|
||||
onclick={() => assetViewerManager.closeDetailPanel()}
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
/>
|
||||
<p class="text-lg text-immich-fg dark:text-immich-dark-fg">{$t('info')}</p>
|
||||
</div>
|
||||
{#if !assetViewerManager.isEditFacesPanelOpen}
|
||||
<section class="relative p-2">
|
||||
<div class="flex place-items-center gap-2">
|
||||
<IconButton
|
||||
icon={mdiClose}
|
||||
aria-label={$t('close')}
|
||||
onclick={() => assetViewerManager.closeDetailPanel()}
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
/>
|
||||
<p class="text-lg text-immich-fg dark:text-immich-dark-fg">{$t('info')}</p>
|
||||
</div>
|
||||
|
||||
{#if asset.isOffline}
|
||||
<section class="px-4 py-4">
|
||||
<div role="alert">
|
||||
<div class="rounded-t bg-red-500 px-4 py-2 font-bold text-white">
|
||||
{$t('asset_offline')}
|
||||
{#if asset.isOffline}
|
||||
<section class="px-4 py-4">
|
||||
<div role="alert">
|
||||
<div class="rounded-t bg-red-500 px-4 py-2 font-bold text-white">
|
||||
{$t('asset_offline')}
|
||||
</div>
|
||||
<div class="border border-t-0 border-red-400 bg-red-100 px-4 py-3 text-red-700">
|
||||
<p>
|
||||
{#if authManager.authenticated && authManager.user.isAdmin}
|
||||
{$t('admin.asset_offline_description')}
|
||||
{:else}
|
||||
{$t('asset_offline_description')}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-b bg-red-500 px-4 py-2 text-white text-sm">
|
||||
<p>{asset.originalPath}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border border-t-0 border-red-400 bg-red-100 px-4 py-3 text-red-700">
|
||||
<p>
|
||||
{#if authManager.authenticated && authManager.user.isAdmin}
|
||||
{$t('admin.asset_offline_description')}
|
||||
{:else}
|
||||
{$t('asset_offline_description')}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<DetailPanelDescription {asset} {isOwner} />
|
||||
<DetailPanelRating {asset} {isOwner} />
|
||||
|
||||
{#if !authManager.isSharedLink && isOwner}
|
||||
<section class="px-4 pt-4 text-sm">
|
||||
<div class="flex h-10 w-full items-center justify-between">
|
||||
<Text size="small" color="muted">{$t('people')}</Text>
|
||||
<div class="flex gap-2 items-center">
|
||||
{#if people.some((person) => person.isHidden)}
|
||||
<IconButton
|
||||
aria-label={$t('show_hidden_people')}
|
||||
icon={showingHiddenPeople ? mdiEyeOff : mdiEye}
|
||||
size="medium"
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
onclick={() => (showingHiddenPeople = !showingHiddenPeople)}
|
||||
/>
|
||||
{/if}
|
||||
<IconButton
|
||||
aria-label={$t('tag_people')}
|
||||
icon={mdiPlus}
|
||||
size="medium"
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
onclick={() => assetViewerManager.toggleFaceEditMode()}
|
||||
/>
|
||||
|
||||
{#if people.length > 0 || unassignedFaces.length > 0}
|
||||
<IconButton
|
||||
aria-label={$t('edit_people')}
|
||||
icon={mdiPencil}
|
||||
size="medium"
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
onclick={() => assetViewerManager.openEditFacesPanel()}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
{#each people as person, index (person.id)}
|
||||
{#if showingHiddenPeople || !person.isHidden}
|
||||
{@const isHighlighted = people[index].faces.some((f) => $boundingBoxesArray.some((b) => b.id === f.id))}
|
||||
<a
|
||||
class="group w-22 outline-none"
|
||||
href={Route.viewPerson(person, { previousRoute })}
|
||||
onfocus={() => ($boundingBoxesArray = people[index].faces)}
|
||||
onblur={() => ($boundingBoxesArray = [])}
|
||||
onmouseover={() => ($boundingBoxesArray = people[index].faces)}
|
||||
onmouseleave={() => ($boundingBoxesArray = [])}
|
||||
>
|
||||
<div class="relative">
|
||||
<ImageThumbnail
|
||||
curve
|
||||
shadow
|
||||
url={getPeopleThumbnailUrl(person)}
|
||||
altText={person.name}
|
||||
title={person.name}
|
||||
widthStyle="90px"
|
||||
heightStyle="90px"
|
||||
hidden={person.isHidden}
|
||||
highlighted={isHighlighted}
|
||||
class="group-focus-visible:outline-2 group-focus-visible:outline-offset-2 group-focus-visible:outline-immich-primary dark:group-focus-visible:outline-immich-dark-primary"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-1 truncate font-medium" title={person.name}>{person.name}</p>
|
||||
{#if person.birthDate}
|
||||
{@const personBirthDate = DateTime.fromISO(person.birthDate)}
|
||||
{@const age = Math.floor(DateTime.fromISO(asset.localDateTime).diff(personBirthDate, 'years').years)}
|
||||
{@const ageInMonths = Math.floor(
|
||||
DateTime.fromISO(asset.localDateTime).diff(personBirthDate, 'months').months,
|
||||
)}
|
||||
{#if age >= 0}
|
||||
<p
|
||||
class="font-light"
|
||||
title={personBirthDate.toLocaleString(
|
||||
{
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
},
|
||||
{ locale: $locale },
|
||||
)}
|
||||
>
|
||||
{#if ageInMonths <= 11}
|
||||
{$t('age_months', { values: { months: ageInMonths } })}
|
||||
{:else if ageInMonths > 12 && ageInMonths <= 23}
|
||||
{$t('age_year_months', { values: { months: ageInMonths - 12 } })}
|
||||
{:else}
|
||||
{$t('age_years', { values: { years: age } })}
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<div class="px-4 py-4">
|
||||
{#if asset.exifInfo}
|
||||
<div class="flex h-10 w-full items-center justify-between text-sm">
|
||||
<Text size="small" color="muted">{$t('details')}</Text>
|
||||
</div>
|
||||
{:else}
|
||||
<Text size="small" color="muted">{$t('no_exif_info_available')}</Text>
|
||||
{/if}
|
||||
|
||||
<DetailPanelDate {asset} />
|
||||
|
||||
<div class="flex gap-4 py-4">
|
||||
<div><Icon icon={mdiImageOutline} size="24" /></div>
|
||||
|
||||
<div>
|
||||
<p class="break-all flex place-items-center gap-2 whitespace-pre-wrap">
|
||||
{asset.originalFileName}
|
||||
{#if isOwner}
|
||||
<IconButton
|
||||
icon={mdiInformationOutline}
|
||||
aria-label={$t('show_file_location')}
|
||||
size="small"
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
onclick={() => assetViewerManager.toggleAssetPath()}
|
||||
/>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-b bg-red-500 px-4 py-2 text-white text-sm">
|
||||
<p>{asset.originalPath}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<DetailPanelDescription {asset} {isOwner} />
|
||||
<DetailPanelRating {asset} {isOwner} />
|
||||
|
||||
{#if !authManager.isSharedLink && isOwner}
|
||||
<section class="px-4 pt-4 text-sm">
|
||||
<div class="flex h-10 w-full items-center justify-between">
|
||||
<Text size="small" color="muted">{$t('people')}</Text>
|
||||
<div class="flex gap-2 items-center">
|
||||
{#if people.some((person) => person.isHidden)}
|
||||
<IconButton
|
||||
aria-label={$t('show_hidden_people')}
|
||||
icon={showingHiddenPeople ? mdiEyeOff : mdiEye}
|
||||
size="medium"
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
onclick={() => (showingHiddenPeople = !showingHiddenPeople)}
|
||||
/>
|
||||
{#if assetViewerManager.isShowAssetPath}
|
||||
<p class="text-xs opacity-50 break-all pb-2 hover:text-primary" transition:slide={{ duration: 250 }}>
|
||||
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve this is supposed to be treated as an absolute/external link -->
|
||||
<a href={getAssetFolderHref(asset)} title={$t('go_to_folder')} class="whitespace-pre-wrap">
|
||||
{asset.originalPath}
|
||||
</a>
|
||||
</p>
|
||||
{/if}
|
||||
<IconButton
|
||||
aria-label={$t('tag_people')}
|
||||
icon={mdiPlus}
|
||||
size="medium"
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
onclick={() => assetViewerManager.toggleFaceEditMode()}
|
||||
/>
|
||||
|
||||
{#if people.length > 0 || unassignedFaces.length > 0}
|
||||
<IconButton
|
||||
aria-label={$t('edit_people')}
|
||||
icon={mdiPencil}
|
||||
size="medium"
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
onclick={() => assetViewerManager.openEditFacesPanel()}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
{#each people as person, index (person.id)}
|
||||
{#if showingHiddenPeople || !person.isHidden}
|
||||
{@const isHighlighted = people[index].faces.some((f) => $boundingBoxesArray.some((b) => b.id === f.id))}
|
||||
<a
|
||||
class="group w-22 outline-none"
|
||||
href={Route.viewPerson(person, { previousRoute })}
|
||||
onfocus={() => ($boundingBoxesArray = people[index].faces)}
|
||||
onblur={() => ($boundingBoxesArray = [])}
|
||||
onmouseover={() => ($boundingBoxesArray = people[index].faces)}
|
||||
onmouseleave={() => ($boundingBoxesArray = [])}
|
||||
>
|
||||
<div class="relative">
|
||||
<ImageThumbnail
|
||||
curve
|
||||
shadow
|
||||
url={getPeopleThumbnailUrl(person)}
|
||||
altText={person.name}
|
||||
title={person.name}
|
||||
widthStyle="90px"
|
||||
heightStyle="90px"
|
||||
hidden={person.isHidden}
|
||||
highlighted={isHighlighted}
|
||||
class="group-focus-visible:outline-2 group-focus-visible:outline-offset-2 group-focus-visible:outline-immich-primary dark:group-focus-visible:outline-immich-dark-primary"
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-1 truncate font-medium" title={person.name}>{person.name}</p>
|
||||
{#if person.birthDate}
|
||||
{@const personBirthDate = DateTime.fromISO(person.birthDate)}
|
||||
{@const age = Math.floor(DateTime.fromISO(asset.localDateTime).diff(personBirthDate, 'years').years)}
|
||||
{@const ageInMonths = Math.floor(
|
||||
DateTime.fromISO(asset.localDateTime).diff(personBirthDate, 'months').months,
|
||||
)}
|
||||
{#if age >= 0}
|
||||
<p
|
||||
class="font-light"
|
||||
title={personBirthDate.toLocaleString(
|
||||
{
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
},
|
||||
{ locale: $locale },
|
||||
)}
|
||||
>
|
||||
{#if ageInMonths <= 11}
|
||||
{$t('age_months', { values: { months: ageInMonths } })}
|
||||
{:else if ageInMonths > 12 && ageInMonths <= 23}
|
||||
{$t('age_year_months', { values: { months: ageInMonths - 12 } })}
|
||||
{:else}
|
||||
{$t('age_years', { values: { years: age } })}
|
||||
{/if}
|
||||
{#if (asset.exifInfo?.exifImageHeight && asset.exifInfo?.exifImageWidth) || asset.exifInfo?.fileSizeInByte}
|
||||
<div class="flex gap-2 text-sm">
|
||||
{#if asset.exifInfo?.exifImageHeight && asset.exifInfo?.exifImageWidth}
|
||||
{#if getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)}
|
||||
<p>
|
||||
{getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)} MP
|
||||
</p>
|
||||
{/if}
|
||||
{@const { width, height } = getDimensions(asset.exifInfo)}
|
||||
<p>{width} x {height}</p>
|
||||
{/if}
|
||||
</a>
|
||||
{#if asset.exifInfo?.fileSizeInByte}
|
||||
<p>{getByteUnitString(asset.exifInfo.fileSizeInByte, $locale)}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if asset.exifInfo?.make || asset.exifInfo?.model || asset.exifInfo?.exposureTime || asset.exifInfo?.iso}
|
||||
<div class="flex gap-4 py-4">
|
||||
<div><Icon icon={mdiCamera} size="24" /></div>
|
||||
|
||||
<div>
|
||||
{#if asset.exifInfo?.make || asset.exifInfo?.model}
|
||||
<p>
|
||||
<a
|
||||
href={Route.search({
|
||||
make: asset.exifInfo?.make ?? undefined,
|
||||
model: asset.exifInfo?.model ?? undefined,
|
||||
})}
|
||||
title="{$t('search_for')} {asset.exifInfo.make || ''} {asset.exifInfo.model || ''}"
|
||||
class="hover:text-primary"
|
||||
>
|
||||
{asset.exifInfo.make || ''}
|
||||
{asset.exifInfo.model || ''}
|
||||
</a>
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-2 text-sm">
|
||||
{#if asset.exifInfo.exposureTime}
|
||||
<p>{`${asset.exifInfo.exposureTime} s`}</p>
|
||||
{/if}
|
||||
|
||||
{#if asset.exifInfo.iso}
|
||||
<p>{`ISO ${asset.exifInfo.iso}`}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if asset.exifInfo?.lensModel || asset.exifInfo?.fNumber || asset.exifInfo?.focalLength}
|
||||
<div class="flex gap-4 py-4">
|
||||
<div><Icon icon={mdiCameraIris} size="24" /></div>
|
||||
|
||||
<div>
|
||||
{#if asset.exifInfo?.lensModel}
|
||||
<p>
|
||||
<a
|
||||
href={Route.search({ lensModel: asset.exifInfo.lensModel })}
|
||||
title="{$t('search_for')} {asset.exifInfo.lensModel}"
|
||||
class="hover:text-primary line-clamp-1"
|
||||
>
|
||||
{asset.exifInfo.lensModel}
|
||||
</a>
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-2 text-sm">
|
||||
{#if asset.exifInfo?.fNumber}
|
||||
<p>ƒ/{asset.exifInfo.fNumber.toLocaleString($locale)}</p>
|
||||
{/if}
|
||||
|
||||
{#if asset.exifInfo.focalLength}
|
||||
<p>{`${asset.exifInfo.focalLength.toLocaleString($locale)} mm`}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<DetailPanelLocation {isOwner} {asset} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#if latlng && featureFlagsManager.value.map}
|
||||
<div class="h-90">
|
||||
{#await import('$lib/components/shared-components/map/map.svelte')}
|
||||
{#await delay(timeToLoadTheMap) then}
|
||||
<!-- show the loading spinner only if loading the map takes too much time -->
|
||||
<div class="flex items-center justify-center h-full w-full">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
{/await}
|
||||
{:then { default: Map }}
|
||||
<Map
|
||||
mapMarkers={[
|
||||
{
|
||||
lat: latlng.lat,
|
||||
lon: latlng.lng,
|
||||
id: asset.id,
|
||||
city: asset.exifInfo?.city ?? null,
|
||||
state: asset.exifInfo?.state ?? null,
|
||||
country: asset.exifInfo?.country ?? null,
|
||||
},
|
||||
]}
|
||||
center={latlng}
|
||||
showSettings={false}
|
||||
zoom={12.5}
|
||||
simplified
|
||||
useLocationPin
|
||||
showSimpleControls={!assetViewerManager.isEditFacesPanelOpen}
|
||||
onOpenInMapView={() => goto(Route.map({ ...latlng, zoom: 12.5 }))}
|
||||
>
|
||||
{#snippet popup({ marker })}
|
||||
{@const { lat, lon } = marker}
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<p class="font-bold">{lat.toPrecision(6)}, {lon.toPrecision(6)}</p>
|
||||
<a
|
||||
href="https://www.openstreetmap.org/?mlat={lat}&mlon={lon}&zoom=13#map=15/{lat}/{lon}"
|
||||
target="_blank"
|
||||
class="font-medium text-primary underline focus:outline-none"
|
||||
>
|
||||
{$t('open_in_openstreetmap')}
|
||||
</a>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Map>
|
||||
{/await}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if currentAlbum && currentAlbum.albumUsers.length > 0 && asset.owner}
|
||||
<section class="px-6 dark:text-immich-dark-fg mt-4">
|
||||
<Text size="small" color="muted">{$t('shared_by')}</Text>
|
||||
<div class="flex gap-4 pt-4">
|
||||
<div>
|
||||
<UserAvatar user={asset.owner} size="md" />
|
||||
</div>
|
||||
|
||||
<div class="mb-auto mt-auto">
|
||||
<p>
|
||||
{asset.owner.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<div class="px-4 py-4">
|
||||
{#if asset.exifInfo}
|
||||
<div class="flex h-10 w-full items-center justify-between text-sm">
|
||||
<Text size="small" color="muted">{$t('details')}</Text>
|
||||
</div>
|
||||
{:else}
|
||||
<Text size="small" color="muted">{$t('no_exif_info_available')}</Text>
|
||||
{/if}
|
||||
|
||||
<DetailPanelDate {asset} />
|
||||
|
||||
<div class="flex gap-4 py-4">
|
||||
<div><Icon icon={mdiImageOutline} size="24" /></div>
|
||||
|
||||
<div>
|
||||
<p class="break-all flex place-items-center gap-2 whitespace-pre-wrap">
|
||||
{asset.originalFileName}
|
||||
{#if isOwner}
|
||||
<IconButton
|
||||
icon={mdiInformationOutline}
|
||||
aria-label={$t('show_file_location')}
|
||||
size="small"
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
onclick={() => assetViewerManager.toggleAssetPath()}
|
||||
/>
|
||||
{/if}
|
||||
</p>
|
||||
{#if assetViewerManager.isShowAssetPath}
|
||||
<p class="text-xs opacity-50 break-all pb-2 hover:text-primary" transition:slide={{ duration: 250 }}>
|
||||
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve this is supposed to be treated as an absolute/external link -->
|
||||
<a href={getAssetFolderHref(asset)} title={$t('go_to_folder')} class="whitespace-pre-wrap">
|
||||
{asset.originalPath}
|
||||
</a>
|
||||
</p>
|
||||
{/if}
|
||||
{#if (asset.exifInfo?.exifImageHeight && asset.exifInfo?.exifImageWidth) || asset.exifInfo?.fileSizeInByte}
|
||||
<div class="flex gap-2 text-sm">
|
||||
{#if asset.exifInfo?.exifImageHeight && asset.exifInfo?.exifImageWidth}
|
||||
{#if getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)}
|
||||
<p>
|
||||
{getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)} MP
|
||||
</p>
|
||||
{/if}
|
||||
{@const { width, height } = getDimensions(asset.exifInfo)}
|
||||
<p>{width} x {height}</p>
|
||||
{/if}
|
||||
{#if asset.exifInfo?.fileSizeInByte}
|
||||
<p>{getByteUnitString(asset.exifInfo.fileSizeInByte, $locale)}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if asset.exifInfo?.make || asset.exifInfo?.model || asset.exifInfo?.exposureTime || asset.exifInfo?.iso}
|
||||
<div class="flex gap-4 py-4">
|
||||
<div><Icon icon={mdiCamera} size="24" /></div>
|
||||
|
||||
<div>
|
||||
{#if asset.exifInfo?.make || asset.exifInfo?.model}
|
||||
<p>
|
||||
<a
|
||||
href={Route.search({
|
||||
make: asset.exifInfo?.make ?? undefined,
|
||||
model: asset.exifInfo?.model ?? undefined,
|
||||
})}
|
||||
title="{$t('search_for')} {asset.exifInfo.make || ''} {asset.exifInfo.model || ''}"
|
||||
class="hover:text-primary"
|
||||
>
|
||||
{asset.exifInfo.make || ''}
|
||||
{asset.exifInfo.model || ''}
|
||||
</a>
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-2 text-sm">
|
||||
{#if asset.exifInfo.exposureTime}
|
||||
<p>{`${asset.exifInfo.exposureTime} s`}</p>
|
||||
{/if}
|
||||
|
||||
{#if asset.exifInfo.iso}
|
||||
<p>{`ISO ${asset.exifInfo.iso}`}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{#await albums then albums}
|
||||
{#if albums.length > 0}
|
||||
<section class="px-6 py-6 dark:text-immich-dark-fg">
|
||||
<div class="pb-4">
|
||||
<Text size="small" color="muted">{$t('appears_in')}</Text>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#each albums as album (album.id)}
|
||||
<a href={Route.viewAlbum(album)}>
|
||||
<div class="flex gap-4 pt-2 hover:cursor-pointer items-center">
|
||||
<div>
|
||||
<img
|
||||
alt={album.albumName}
|
||||
class="h-12.5 w-12.5 rounded object-cover"
|
||||
src={album.albumThumbnailAssetId &&
|
||||
getAssetMediaUrl({ id: album.albumThumbnailAssetId, size: AssetMediaSize.Preview })}
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if asset.exifInfo?.lensModel || asset.exifInfo?.fNumber || asset.exifInfo?.focalLength}
|
||||
<div class="flex gap-4 py-4">
|
||||
<div><Icon icon={mdiCameraIris} size="24" /></div>
|
||||
|
||||
<div>
|
||||
{#if asset.exifInfo?.lensModel}
|
||||
<p>
|
||||
<a
|
||||
href={Route.search({ lensModel: asset.exifInfo.lensModel })}
|
||||
title="{$t('search_for')} {asset.exifInfo.lensModel}"
|
||||
class="hover:text-primary line-clamp-1"
|
||||
>
|
||||
{asset.exifInfo.lensModel}
|
||||
</a>
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-2 text-sm">
|
||||
{#if asset.exifInfo?.fNumber}
|
||||
<p>ƒ/{asset.exifInfo.fNumber.toLocaleString($locale)}</p>
|
||||
{/if}
|
||||
|
||||
{#if asset.exifInfo.focalLength}
|
||||
<p>{`${asset.exifInfo.focalLength.toLocaleString($locale)} mm`}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<DetailPanelLocation {isOwner} {asset} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#if latlng && featureFlagsManager.value.map}
|
||||
<div class="h-90">
|
||||
{#await import('$lib/components/shared-components/map/map.svelte')}
|
||||
{#await delay(timeToLoadTheMap) then}
|
||||
<!-- show the loading spinner only if loading the map takes too much time -->
|
||||
<div class="flex items-center justify-center h-full w-full">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
{/await}
|
||||
{:then { default: Map }}
|
||||
<Map
|
||||
mapMarkers={[
|
||||
{
|
||||
lat: latlng.lat,
|
||||
lon: latlng.lng,
|
||||
id: asset.id,
|
||||
city: asset.exifInfo?.city ?? null,
|
||||
state: asset.exifInfo?.state ?? null,
|
||||
country: asset.exifInfo?.country ?? null,
|
||||
},
|
||||
]}
|
||||
center={latlng}
|
||||
showSettings={false}
|
||||
zoom={12.5}
|
||||
simplified
|
||||
useLocationPin
|
||||
showSimpleControls={!showEditFaces}
|
||||
onOpenInMapView={() => goto(Route.map({ ...latlng, zoom: 12.5 }))}
|
||||
>
|
||||
{#snippet popup({ marker })}
|
||||
{@const { lat, lon } = marker}
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<p class="font-bold">{lat.toPrecision(6)}, {lon.toPrecision(6)}</p>
|
||||
<a
|
||||
href="https://www.openstreetmap.org/?mlat={lat}&mlon={lon}&zoom=13#map=15/{lat}/{lon}"
|
||||
target="_blank"
|
||||
class="font-medium text-primary underline focus:outline-none"
|
||||
>
|
||||
{$t('open_in_openstreetmap')}
|
||||
</a>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Map>
|
||||
{/await}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if currentAlbum && currentAlbum.albumUsers.length > 0 && asset.owner}
|
||||
<section class="px-6 dark:text-immich-dark-fg mt-4">
|
||||
<Text size="small" color="muted">{$t('shared_by')}</Text>
|
||||
<div class="flex gap-4 pt-4">
|
||||
<div>
|
||||
<UserAvatar user={asset.owner} size="md" />
|
||||
</div>
|
||||
|
||||
<div class="mb-auto mt-auto">
|
||||
<p>
|
||||
{asset.owner.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#await albums then albums}
|
||||
{#if albums.length > 0}
|
||||
<section class="px-6 py-6 dark:text-immich-dark-fg">
|
||||
<div class="pb-4">
|
||||
<Text size="small" color="muted">{$t('appears_in')}</Text>
|
||||
</div>
|
||||
{#each albums as album (album.id)}
|
||||
<a href={Route.viewAlbum(album)}>
|
||||
<div class="flex gap-4 pt-2 hover:cursor-pointer items-center">
|
||||
<div>
|
||||
<img
|
||||
alt={album.albumName}
|
||||
class="h-12.5 w-12.5 rounded object-cover"
|
||||
src={album.albumThumbnailAssetId &&
|
||||
getAssetMediaUrl({ id: album.albumThumbnailAssetId, size: AssetMediaSize.Preview })}
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-auto mt-auto">
|
||||
<p class="dark:text-immich-dark-primary">{album.albumName}</p>
|
||||
<div class="flex flex-col gap-0 text-sm">
|
||||
<div>
|
||||
<AlbumListItemDetails {album} />
|
||||
<div class="mb-auto mt-auto">
|
||||
<p class="dark:text-immich-dark-primary">{album.albumName}</p>
|
||||
<div class="flex flex-col gap-0 text-sm">
|
||||
<div>
|
||||
<AlbumListItemDetails {album} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</a>
|
||||
{/each}
|
||||
</section>
|
||||
{/if}
|
||||
{/await}
|
||||
|
||||
{#if authManager.authenticated && authManager.preferences.tags.enabled}
|
||||
<section class="relative px-2 pb-12 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
|
||||
<DetailPanelTags {asset} {isOwner} />
|
||||
</section>
|
||||
{/if}
|
||||
{/await}
|
||||
|
||||
{#if authManager.authenticated && authManager.preferences.tags.enabled}
|
||||
<section class="relative px-2 pb-12 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
|
||||
<DetailPanelTags {asset} {isOwner} />
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if showEditFaces}
|
||||
{#if assetViewerManager.isEditFacesPanelOpen}
|
||||
<PersonSidePanel
|
||||
assetId={asset.id}
|
||||
assetType={asset.type}
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
style:inset-inline-start={position.left + 'px'}
|
||||
style:width={position.width + 'px'}
|
||||
style:height={position.height + 'px'}
|
||||
out:scale|global={{ start: 0.1, duration: scaleDuration }}
|
||||
animate:flip={{ duration: transitionDuration }}
|
||||
>
|
||||
{@render thumbnail({ asset, position })}
|
||||
|
||||
@@ -127,9 +127,27 @@
|
||||
const scrollY = $derived(
|
||||
toScrollFromTimelineMonthPercentage(viewportTopMonth, viewportTopMonthScrollPercent, timelineScrollPercent),
|
||||
);
|
||||
const timelineFullHeight = $derived(timelineManager.scrubberTimelineHeight);
|
||||
const relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight));
|
||||
const relativeBottomOffset = $derived(toScrollY(timelineBottomOffset / timelineFullHeight));
|
||||
const estimateMonthHeight = (assetCount: number) => {
|
||||
const viewportWidth = timelineManager.viewportWidth;
|
||||
const rowHeight = timelineManager.rowHeight;
|
||||
const headerHeight = timelineManager.headerHeight;
|
||||
if (viewportWidth === 0) {
|
||||
return headerHeight + rowHeight;
|
||||
}
|
||||
const rows = Math.ceil(((3 / 2) * assetCount * rowHeight * (7 / 10)) / viewportWidth);
|
||||
return headerHeight + Math.max(1, rows) * rowHeight;
|
||||
};
|
||||
|
||||
const totalEstimatedHeight = $derived.by(() => {
|
||||
let total = timelineManager.topSectionHeight + timelineManager.bottomSectionHeight;
|
||||
for (const month of timelineManager.scrubberMonths) {
|
||||
total += estimateMonthHeight(month.assetCount);
|
||||
}
|
||||
return total;
|
||||
});
|
||||
|
||||
const relativeTopOffset = $derived(toScrollY(timelineManager.topSectionHeight / totalEstimatedHeight));
|
||||
const relativeBottomOffset = $derived(toScrollY(timelineManager.bottomSectionHeight / totalEstimatedHeight));
|
||||
|
||||
type Segment = {
|
||||
count: number;
|
||||
@@ -154,7 +172,7 @@
|
||||
const reversed = [...months].reverse();
|
||||
|
||||
for (const scrubMonth of reversed) {
|
||||
const scrollBarPercentage = scrubMonth.height / timelineFullHeight;
|
||||
const scrollBarPercentage = estimateMonthHeight(scrubMonth.assetCount) / totalEstimatedHeight;
|
||||
|
||||
const segment = {
|
||||
top,
|
||||
@@ -493,7 +511,13 @@
|
||||
|
||||
<svelte:window
|
||||
bind:innerHeight={windowHeight}
|
||||
onmousemove={({ clientY }) => (isDragging || isHover) && handleMouseEvent({ clientY })}
|
||||
onmousemove={(e) => {
|
||||
if (isDragging && (e.buttons & 1) === 0) {
|
||||
handleMouseEvent({ clientY: e.clientY, isDragging: false });
|
||||
} else if (isDragging || isHover) {
|
||||
handleMouseEvent({ clientY: e.clientY });
|
||||
}
|
||||
}}
|
||||
onmousedown={({ clientY }) => isHover && handleMouseEvent({ clientY, isDragging: true })}
|
||||
onmouseup={({ clientY }) => handleMouseEvent({ clientY, isDragging: false })}
|
||||
/>
|
||||
|
||||
@@ -13,16 +13,17 @@
|
||||
import Skeleton from '$lib/elements/Skeleton.svelte';
|
||||
import type { AssetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte';
|
||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||
import type { TimelineDay } from '$lib/managers/timeline-manager/timeline-day.svelte';
|
||||
import { isIntersecting } from '$lib/managers/timeline-manager/internal/intersection-support.svelte';
|
||||
import type { TimelineMonth } from '$lib/managers/timeline-manager/timeline-month.svelte';
|
||||
import type { TimelineDay } from '$lib/managers/timeline-manager/timeline-day.svelte';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import type { TimelineMonth } from '$lib/managers/timeline-manager/timeline-month.svelte';
|
||||
import type { TimelineAsset, TimelineManagerOptions, ViewportTopMonth } from '$lib/managers/timeline-manager/types';
|
||||
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
||||
import { mediaQueryManager } from '$lib/stores/media-query-manager.svelte';
|
||||
import { isAssetViewerRoute, navigate } from '$lib/utils/navigation';
|
||||
import { getTimes, type ScrubberListener } from '$lib/utils/timeline-util';
|
||||
import { type AlbumResponseDto, type PersonResponseDto, type UserResponseDto } from '@immich/sdk';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { DateTime } from 'luxon';
|
||||
import { onDestroy, onMount, tick, type Snippet } from 'svelte';
|
||||
import type { UpdatePayload } from 'vite';
|
||||
@@ -101,6 +102,14 @@
|
||||
let scrubberWidth = $state(0);
|
||||
|
||||
const isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
|
||||
const topSectionPlaneTop = $derived(
|
||||
timelineManager.months.length > 0 ? timelineManager.months[0].planeTop - timelineManager.topSectionHeight : 0,
|
||||
);
|
||||
const leadoutPlaneTop = $derived(
|
||||
timelineManager.months.length > 0
|
||||
? timelineManager.months.at(-1)!.planeTop + timelineManager.months.at(-1)!.height
|
||||
: timelineManager.topSectionHeight,
|
||||
);
|
||||
const maxMd = $derived(mediaQueryManager.maxMd);
|
||||
const usingMobileDevice = $derived(mediaQueryManager.pointerCoarse);
|
||||
|
||||
@@ -169,18 +178,15 @@
|
||||
|
||||
const scrollAndLoadAsset = async (assetId: string) => {
|
||||
try {
|
||||
// This flag prevents layout deferral to fix scroll positioning issues.
|
||||
// When layouts are deferred and we scroll to an asset at the end of the timeline,
|
||||
// we can calculate the asset's position, but the scrollableElement's scrollHeight
|
||||
// hasn't been updated yet to reflect the new layout. This creates a mismatch that
|
||||
// breaks scroll positioning. By disabling layout deferral in this case, we maintain
|
||||
// the performance benefits of deferred layouts while still supporting deep linking
|
||||
// to assets at the end of the timeline.
|
||||
timelineManager.isScrollingOnLoad = true;
|
||||
const timelineMonth = await timelineManager.findTimelineMonthForAsset({ id: assetId });
|
||||
if (!timelineMonth) {
|
||||
return false;
|
||||
}
|
||||
const monthIndex = timelineManager.months.indexOf(timelineMonth);
|
||||
if (monthIndex !== -1) {
|
||||
timelineManager.jumpToMonth({ monthIndex, fractionInMonth: 0 });
|
||||
}
|
||||
scrollToAssetPosition(assetId, timelineMonth);
|
||||
return true;
|
||||
} finally {
|
||||
@@ -213,7 +219,6 @@
|
||||
scrolled = await scrollAndLoadAsset(scrollTarget);
|
||||
}
|
||||
if (!scrolled) {
|
||||
// if the asset is not found, scroll to the top
|
||||
timelineManager.scrollTo(0);
|
||||
} else if (scrollTarget) {
|
||||
await tick();
|
||||
@@ -263,102 +268,105 @@
|
||||
}
|
||||
});
|
||||
|
||||
const scrollToSegmentPercentage = (segmentTop: number, segmentHeight: number, timelineMonthScrollPercent: number) => {
|
||||
const topOffset = segmentTop;
|
||||
const maxScrollPercent = timelineManager.maxScrollPercent;
|
||||
const delta = segmentHeight * timelineMonthScrollPercent;
|
||||
const scrollToTop = (topOffset + delta) * maxScrollPercent;
|
||||
|
||||
timelineManager.scrollTo(scrollToTop);
|
||||
};
|
||||
|
||||
// note: don't throttle, debounce, or otherwise make this function async - it causes flicker
|
||||
// this function scrolls the timeline to the specified month group and offset, based on scrubber interaction
|
||||
const onScrub: ScrubberListener = (scrubberData) => {
|
||||
const { scrubberMonth, overallScrollPercent, scrubberMonthScrollPercent } = scrubberData;
|
||||
const { scrubberMonth, scrubberMonthScrollPercent } = scrubberData;
|
||||
|
||||
const leadIn = scrubberMonth === 'lead-in';
|
||||
const leadOut = scrubberMonth === 'lead-out';
|
||||
const noMonth = !scrubberMonth;
|
||||
// For small timelines, use linear percentage for bi-directional sync with handleTimelineScroll
|
||||
if (timelineManager.limitedScroll) {
|
||||
const maxScroll = timelineManager.planeHeight - timelineManager.viewportHeight;
|
||||
timelineManager.scrollTo(scrubberData.overallScrollPercent * maxScroll);
|
||||
return;
|
||||
}
|
||||
|
||||
if (noMonth || timelineManager.limitedScroll) {
|
||||
// edge case - scroll limited due to size of content, must adjust - use use the overall percent instead
|
||||
const maxScroll = timelineManager.maxScrollPercent;
|
||||
const offset = maxScroll * overallScrollPercent * timelineManager.totalViewerHeight;
|
||||
timelineManager.scrollTo(offset);
|
||||
} else if (leadIn) {
|
||||
scrollToSegmentPercentage(0, timelineManager.topSectionHeight, scrubberMonthScrollPercent);
|
||||
} else if (leadOut) {
|
||||
scrollToSegmentPercentage(
|
||||
timelineManager.topSectionHeight + timelineManager.bodySectionHeight,
|
||||
timelineManager.bottomSectionHeight,
|
||||
scrubberMonthScrollPercent,
|
||||
);
|
||||
} else {
|
||||
const timelineMonth = timelineManager.months.find(
|
||||
({ yearMonth: { year, month } }) => year === scrubberMonth.year && month === scrubberMonth.month,
|
||||
);
|
||||
if (!timelineMonth) {
|
||||
if (!scrubberMonth) {
|
||||
if (timelineManager.months.length === 0) {
|
||||
return;
|
||||
}
|
||||
scrollToSegmentPercentage(timelineMonth.top, timelineMonth.height, scrubberMonthScrollPercent);
|
||||
if (scrubberData.overallScrollPercent <= 0) {
|
||||
const firstMonth = timelineManager.months[0];
|
||||
timelineManager.scrollTo(firstMonth.planeTop - timelineManager.topSectionHeight);
|
||||
} else if (scrubberData.overallScrollPercent >= 1) {
|
||||
const lastMonth = timelineManager.months.at(-1)!;
|
||||
timelineManager.scrollTo(
|
||||
lastMonth.planeTop + lastMonth.height + timelineManager.bottomSectionHeight - timelineManager.viewportHeight,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (scrubberMonth === 'lead-in') {
|
||||
if (timelineManager.months.length > 0) {
|
||||
const firstMonth = timelineManager.months[0];
|
||||
timelineManager.scrollTo(
|
||||
firstMonth.planeTop - timelineManager.topSectionHeight * (1 - scrubberMonthScrollPercent),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (scrubberMonth === 'lead-out') {
|
||||
if (timelineManager.months.length > 0) {
|
||||
const lastMonth = timelineManager.months.at(-1)!;
|
||||
timelineManager.scrollTo(
|
||||
lastMonth.planeTop + lastMonth.height + timelineManager.bottomSectionHeight * scrubberMonthScrollPercent,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const monthIndex = timelineManager.months.findIndex(
|
||||
({ yearMonth: { year, month } }) => year === scrubberMonth.year && month === scrubberMonth.month,
|
||||
);
|
||||
if (monthIndex !== -1) {
|
||||
timelineManager.jumpToMonth({ monthIndex, fractionInMonth: scrubberMonthScrollPercent });
|
||||
}
|
||||
};
|
||||
|
||||
// note: don't throttle, debounce, or otherwise make this function async - it causes flicker
|
||||
const handleTimelineScroll = () => {
|
||||
if (!scrollableElement) {
|
||||
const maxScroll = timelineManager.planeHeight - timelineManager.viewportHeight;
|
||||
timelineScrollPercent = maxScroll > 0 ? clamp(timelineManager.visibleWindow.top / maxScroll, 0, 1) : 0;
|
||||
|
||||
// For small timelines, use linear percentage positioning for smooth bi-directional sync
|
||||
if (timelineManager.limitedScroll) {
|
||||
viewportTopMonth = undefined;
|
||||
viewportTopMonthScrollPercent = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (timelineManager.limitedScroll) {
|
||||
// edge case - scroll limited due to size of content, must adjust - use the overall percent instead
|
||||
const maxScroll = timelineManager.maxScroll;
|
||||
|
||||
timelineScrollPercent = Math.min(1, scrollableElement.scrollTop / maxScroll);
|
||||
const intersection = timelineManager.viewportTopMonthIntersection;
|
||||
if (!intersection?.month) {
|
||||
viewportTopMonth = undefined;
|
||||
viewportTopMonthScrollPercent = 0;
|
||||
} else {
|
||||
timelineScrollPercent = 0;
|
||||
|
||||
let top = scrollableElement.scrollTop;
|
||||
let maxScrollPercent = timelineManager.maxScrollPercent;
|
||||
|
||||
const monthsLength = timelineManager.months.length;
|
||||
for (let i = -1; i < monthsLength + 1; i++) {
|
||||
let timelineMonth: ViewportTopMonth;
|
||||
let timelineMonthHeight: number;
|
||||
if (i === -1) {
|
||||
// lead-in
|
||||
timelineMonth = 'lead-in';
|
||||
timelineMonthHeight = timelineManager.topSectionHeight;
|
||||
} else if (i === monthsLength) {
|
||||
// lead-out
|
||||
timelineMonth = 'lead-out';
|
||||
timelineMonthHeight = timelineManager.bottomSectionHeight;
|
||||
} else {
|
||||
timelineMonth = timelineManager.months[i].yearMonth;
|
||||
timelineMonthHeight = timelineManager.months[i].height;
|
||||
}
|
||||
|
||||
let next = top - timelineMonthHeight * maxScrollPercent;
|
||||
// instead of checking for < 0, add a little wiggle room for subpixel resolution
|
||||
if (next < -1 && timelineMonth) {
|
||||
viewportTopMonth = timelineMonth;
|
||||
|
||||
// allowing next to be at least 1 may cause percent to go negative, so ensure positive percentage
|
||||
viewportTopMonthScrollPercent = Math.max(0, top / (timelineMonthHeight * maxScrollPercent));
|
||||
|
||||
// compensate for lost precision/rounding errors advance to the next bucket, if present
|
||||
if (viewportTopMonthScrollPercent > 0.9999 && i + 1 < monthsLength - 1) {
|
||||
viewportTopMonth = timelineManager.months[i + 1].yearMonth;
|
||||
viewportTopMonthScrollPercent = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
top = next;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const firstMonth = timelineManager.months[0];
|
||||
if (firstMonth && timelineManager.visibleWindow.top < firstMonth.planeTop) {
|
||||
viewportTopMonth = 'lead-in';
|
||||
const topSectionTop = firstMonth.planeTop - timelineManager.topSectionHeight;
|
||||
viewportTopMonthScrollPercent =
|
||||
timelineManager.topSectionHeight > 0
|
||||
? Math.max(0, (timelineManager.visibleWindow.top - topSectionTop) / timelineManager.topSectionHeight)
|
||||
: 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const lastMonth = timelineManager.months.at(-1)!;
|
||||
const contentBottom = lastMonth.planeTop + lastMonth.height;
|
||||
if (timelineManager.visibleWindow.top >= contentBottom && timelineManager.bottomSectionHeight > 0) {
|
||||
viewportTopMonth = 'lead-out';
|
||||
viewportTopMonthScrollPercent = Math.min(
|
||||
1,
|
||||
(timelineManager.visibleWindow.bottom - contentBottom) / timelineManager.bottomSectionHeight,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
viewportTopMonth = intersection.month.yearMonth;
|
||||
viewportTopMonthScrollPercent = intersection.viewportTopRatioInMonth;
|
||||
};
|
||||
|
||||
const handleSelectAsset = (asset: TimelineAsset) => {
|
||||
@@ -594,6 +602,8 @@
|
||||
{viewportTopMonthScrollPercent}
|
||||
{viewportTopMonth}
|
||||
{onScrub}
|
||||
startScrub={() => timelineManager.setScrubbing(true)}
|
||||
stopScrub={() => timelineManager.setScrubbing(false)}
|
||||
bind:scrubberWidth
|
||||
onScrubKeyDown={(evt) => {
|
||||
evt.preventDefault();
|
||||
@@ -622,13 +632,13 @@
|
||||
bind:clientHeight={timelineManager.viewportHeight}
|
||||
bind:clientWidth={timelineManager.viewportWidth}
|
||||
bind:this={scrollableElement}
|
||||
onscroll={() => (handleTimelineScroll(), timelineManager.updateSlidingWindow(), updateIsScrolling())}
|
||||
onscroll={() => (timelineManager.updateSlidingWindow(), handleTimelineScroll(), updateIsScrolling())}
|
||||
>
|
||||
<section
|
||||
bind:this={timelineElement}
|
||||
id="virtual-timeline"
|
||||
class:invisible
|
||||
style:height={timelineManager.totalViewerHeight + 'px'}
|
||||
style:height={timelineManager.planeHeight + 'px'}
|
||||
>
|
||||
<section
|
||||
bind:clientHeight={timelineManager.topSectionHeight}
|
||||
@@ -636,6 +646,7 @@
|
||||
style:position="absolute"
|
||||
style:left="0"
|
||||
style:right="0"
|
||||
style:transform={`translate3d(0,${topSectionPlaneTop}px,0)`}
|
||||
>
|
||||
{@render children?.()}
|
||||
{#if isEmpty}
|
||||
@@ -646,70 +657,66 @@
|
||||
|
||||
{#each timelineManager.months as timelineMonth (timelineMonth.viewId)}
|
||||
{@const isInOrNearViewport = timelineMonth.isInOrNearViewport}
|
||||
{@const absoluteHeight = timelineMonth.top}
|
||||
{@const absoluteHeight = timelineMonth.planeTop}
|
||||
|
||||
{#if !timelineMonth.isLoaded}
|
||||
{#if isInOrNearViewport}
|
||||
<div
|
||||
style:height={timelineMonth.height + 'px'}
|
||||
style:position="absolute"
|
||||
style:transform={`translate3d(0,${absoluteHeight}px,0)`}
|
||||
style:width="100%"
|
||||
>
|
||||
<Skeleton {invisible} height={timelineMonth.height} title={timelineMonth.title} />
|
||||
</div>
|
||||
{:else if isInOrNearViewport}
|
||||
<div
|
||||
class="timeline-month"
|
||||
style:height={timelineMonth.height + 'px'}
|
||||
style:position="absolute"
|
||||
style:transform={`translate3d(0,${absoluteHeight}px,0)`}
|
||||
style:width="100%"
|
||||
>
|
||||
<Month
|
||||
{assetInteraction}
|
||||
{customThumbnailLayout}
|
||||
{singleSelect}
|
||||
{timelineMonth}
|
||||
manager={timelineManager}
|
||||
onTimelineDaySelect={handleGroupSelect}
|
||||
>
|
||||
{#snippet thumbnail({ asset, position, timelineDay, groupIndex })}
|
||||
{@const isAssetSelectionCandidate = assetInteraction.hasSelectionCandidate(asset.id)}
|
||||
{@const isAssetSelected =
|
||||
assetInteraction.hasSelectedAsset(asset.id) || timelineManager.albumAssets.has(asset.id)}
|
||||
{@const isAssetDisabled = timelineManager.albumAssets.has(asset.id)}
|
||||
<Thumbnail
|
||||
showStackedIcon={withStacked}
|
||||
{showArchiveIcon}
|
||||
{asset}
|
||||
{albumUsers}
|
||||
{groupIndex}
|
||||
onClick={(asset) => {
|
||||
if (typeof onThumbnailClick === 'function') {
|
||||
onThumbnailClick(asset, timelineManager, timelineDay, _onClick);
|
||||
} else {
|
||||
_onClick(timelineManager, timelineDay.getAssets(), timelineDay.groupTitle, asset);
|
||||
}
|
||||
}}
|
||||
onSelect={() => {
|
||||
if (isSelectionMode || assetInteraction.selectionActive) {
|
||||
assetSelectHandler(timelineManager, asset, timelineDay.getAssets(), timelineDay.groupTitle);
|
||||
return;
|
||||
}
|
||||
void onSelectAssets(asset);
|
||||
}}
|
||||
onMouseEvent={() => handleSelectAssetCandidates(asset)}
|
||||
onPreview={isSelectionMode || assetInteraction.selectionActive
|
||||
? (asset) => void navigate({ targetRoute: 'current', assetId: asset.id })
|
||||
: undefined}
|
||||
selected={isAssetSelected}
|
||||
selectionCandidate={isAssetSelectionCandidate}
|
||||
disabled={isAssetDisabled}
|
||||
thumbnailWidth={position.width}
|
||||
thumbnailHeight={position.height}
|
||||
/>
|
||||
{/snippet}
|
||||
</Month>
|
||||
{#if timelineMonth.isLoaded}
|
||||
<div class="timeline-month" style:height={timelineMonth.height + 'px'} style:width="100%">
|
||||
<Month
|
||||
{assetInteraction}
|
||||
{customThumbnailLayout}
|
||||
{singleSelect}
|
||||
{timelineMonth}
|
||||
manager={timelineManager}
|
||||
onTimelineDaySelect={handleGroupSelect}
|
||||
>
|
||||
{#snippet thumbnail({ asset, position, timelineDay, groupIndex })}
|
||||
{@const isAssetSelectionCandidate = assetInteraction.hasSelectionCandidate(asset.id)}
|
||||
{@const isAssetSelected =
|
||||
assetInteraction.hasSelectedAsset(asset.id) || timelineManager.albumAssets.has(asset.id)}
|
||||
{@const isAssetDisabled = timelineManager.albumAssets.has(asset.id)}
|
||||
<Thumbnail
|
||||
showStackedIcon={withStacked}
|
||||
{showArchiveIcon}
|
||||
{asset}
|
||||
{albumUsers}
|
||||
{groupIndex}
|
||||
onClick={(asset) => {
|
||||
if (typeof onThumbnailClick === 'function') {
|
||||
onThumbnailClick(asset, timelineManager, timelineDay, _onClick);
|
||||
} else {
|
||||
_onClick(timelineManager, timelineDay.getAssets(), timelineDay.groupTitle, asset);
|
||||
}
|
||||
}}
|
||||
onSelect={() => {
|
||||
if (isSelectionMode || assetInteraction.selectionActive) {
|
||||
assetSelectHandler(timelineManager, asset, timelineDay.getAssets(), timelineDay.groupTitle);
|
||||
return;
|
||||
}
|
||||
void onSelectAssets(asset);
|
||||
}}
|
||||
onMouseEvent={() => handleSelectAssetCandidates(asset)}
|
||||
onPreview={isSelectionMode || assetInteraction.selectionActive
|
||||
? (asset) => void navigate({ targetRoute: 'current', assetId: asset.id })
|
||||
: undefined}
|
||||
selected={isAssetSelected}
|
||||
selectionCandidate={isAssetSelectionCandidate}
|
||||
disabled={isAssetDisabled}
|
||||
thumbnailWidth={position.width}
|
||||
thumbnailHeight={position.height}
|
||||
/>
|
||||
{/snippet}
|
||||
</Month>
|
||||
</div>
|
||||
{:else}
|
||||
<Skeleton {invisible} height={timelineMonth.height} title={timelineMonth.title} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
@@ -719,7 +726,7 @@
|
||||
style:position="absolute"
|
||||
style:left="0"
|
||||
style:right="0"
|
||||
style:transform={`translate3d(0,${timelineManager.topSectionHeight + timelineManager.bodySectionHeight}px,0)`}
|
||||
style:transform={`translate3d(0,${leadoutPlaneTop}px,0)`}
|
||||
></div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -6,11 +6,30 @@ type LayoutOptions = {
|
||||
gap: number;
|
||||
};
|
||||
export abstract class VirtualScrollManager {
|
||||
topSectionHeight = $state(0);
|
||||
static readonly PLANE_SIZE = 500_000;
|
||||
static readonly PLANE_CENTER = 250_000;
|
||||
|
||||
planeHeight = $state(VirtualScrollManager.PLANE_SIZE);
|
||||
#topSectionHeight = $state(0);
|
||||
bodySectionHeight = $state(0);
|
||||
bottomSectionHeight = $state(0);
|
||||
totalViewerHeight = $derived.by(() => this.topSectionHeight + this.bodySectionHeight + this.bottomSectionHeight);
|
||||
|
||||
get topSectionHeight() {
|
||||
return this.#topSectionHeight;
|
||||
}
|
||||
|
||||
set topSectionHeight(value: number) {
|
||||
if (this.#topSectionHeight === value) {
|
||||
return;
|
||||
}
|
||||
const oldValue = this.#topSectionHeight;
|
||||
this.#topSectionHeight = value;
|
||||
this.onTopSectionHeightChanged(oldValue, value);
|
||||
}
|
||||
|
||||
protected onTopSectionHeightChanged(_oldHeight: number, _newHeight: number) {}
|
||||
|
||||
visibleWindow = $derived.by(() => ({
|
||||
top: this.#scrollTop,
|
||||
bottom: this.#scrollTop + this.viewportHeight,
|
||||
|
||||
@@ -209,7 +209,6 @@ class AssetViewerManager extends BaseEventManager<Events> {
|
||||
this.closeFaceEditMode();
|
||||
this.closeEditFacesPanel();
|
||||
}
|
||||
|
||||
setAsset(asset: AssetResponseDto) {
|
||||
this.#viewingAssetStoreState = asset;
|
||||
this.#viewState = true;
|
||||
|
||||
@@ -42,8 +42,8 @@ function calculateViewportProximity(regionTop: number, regionBottom: number, win
|
||||
|
||||
export function updateTimelineMonthViewportProximity(timelineManager: TimelineManager, month: TimelineMonth) {
|
||||
const proximity = calculateViewportProximity(
|
||||
month.top,
|
||||
month.top + month.height,
|
||||
month.planeTop,
|
||||
month.planeTop + month.height,
|
||||
timelineManager.visibleWindow.top,
|
||||
timelineManager.visibleWindow.bottom,
|
||||
);
|
||||
|
||||
@@ -152,6 +152,6 @@ export class TimelineDay {
|
||||
}
|
||||
|
||||
get absoluteTimelineDayTop() {
|
||||
return this.timelineMonth.top + this.#top;
|
||||
return this.timelineMonth.planeTop + this.#top;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,9 +70,17 @@ export class TimelineManager extends VirtualScrollManager {
|
||||
months: TimelineMonth[] = $state([]);
|
||||
albumAssets: Set<string> = new SvelteSet();
|
||||
scrubberMonths: ScrubberMonth[] = $state([]);
|
||||
scrubberTimelineHeight: number = $state(0);
|
||||
viewportTopMonthIntersection: ViewportTopMonthIntersection | undefined;
|
||||
limitedScroll = $derived(this.maxScrollPercent < 0.5);
|
||||
anchorMonthIndex: number = -1;
|
||||
anchorPlaneTop: number = VirtualScrollManager.PLANE_CENTER;
|
||||
#recenterTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
#recentering = false;
|
||||
#scrubbing = false;
|
||||
limitedScroll = $derived(
|
||||
this.months.length > 0 &&
|
||||
this.totalViewerHeight <= VirtualScrollManager.PLANE_SIZE &&
|
||||
this.viewportHeight > this.months.at(-1)!.height + this.bottomSectionHeight,
|
||||
);
|
||||
initTask = new CancellableTask(
|
||||
() => {
|
||||
this.isInitialized = true;
|
||||
@@ -122,6 +130,22 @@ export class TimelineManager extends VirtualScrollManager {
|
||||
return this.#scrollableElement?.scrollTop ?? 0;
|
||||
}
|
||||
|
||||
protected override onTopSectionHeightChanged(oldHeight: number, newHeight: number) {
|
||||
if (this.anchorMonthIndex === -1 || this.months.length === 0) {
|
||||
return;
|
||||
}
|
||||
const delta = newHeight - oldHeight;
|
||||
const scrollTopBefore = this.scrollTop;
|
||||
this.anchorPlaneTop += delta;
|
||||
this.positionMonthsOnPlane();
|
||||
// If the user is still inside the lead-in, no month content is visible to keep
|
||||
// pinned, and shifting scrollTop would push them past the lead-in.
|
||||
if (scrollTopBefore <= oldHeight) {
|
||||
return;
|
||||
}
|
||||
this.scrollBy(delta);
|
||||
}
|
||||
|
||||
set scrollableElement(element: HTMLElement | undefined) {
|
||||
this.#scrollableElement = element;
|
||||
}
|
||||
@@ -189,7 +213,7 @@ export class TimelineManager extends VirtualScrollManager {
|
||||
return 0;
|
||||
}
|
||||
const windowHeight = this.visibleWindow.bottom - this.visibleWindow.top;
|
||||
const bottomOfMonth = month.top + month.height;
|
||||
const bottomOfMonth = month.planeTop + month.height;
|
||||
const bottomOfMonthInViewport = bottomOfMonth - this.visibleWindow.top;
|
||||
return clamp(bottomOfMonthInViewport / windowHeight, 0, 1);
|
||||
}
|
||||
@@ -198,7 +222,7 @@ export class TimelineManager extends VirtualScrollManager {
|
||||
if (!month) {
|
||||
return 0;
|
||||
}
|
||||
return clamp((this.visibleWindow.top - month.top) / month.height, 0, 1);
|
||||
return clamp((this.visibleWindow.top - month.planeTop) / month.height, 0, 1);
|
||||
}
|
||||
|
||||
override updateViewportProximities() {
|
||||
@@ -238,6 +262,177 @@ export class TimelineManager extends VirtualScrollManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives every month's planeTop by walking outward from the anchor. The anchor
|
||||
* stays pinned at anchorPlaneTop, so any height change elsewhere shifts months
|
||||
* away from the anchor — content at the viewport-top stays stable as long as
|
||||
* trackAnchorToViewportTop ran beforehand.
|
||||
*/
|
||||
positionMonthsOnPlane() {
|
||||
if (this.months.length === 0 || this.anchorMonthIndex === -1) {
|
||||
return;
|
||||
}
|
||||
const anchor = this.months[this.anchorMonthIndex];
|
||||
anchor.planeTop = this.anchorPlaneTop;
|
||||
|
||||
let cursorBelow = this.anchorPlaneTop + anchor.height;
|
||||
for (let i = this.anchorMonthIndex + 1; i < this.months.length; i++) {
|
||||
const month = this.months[i];
|
||||
month.planeTop = cursorBelow;
|
||||
cursorBelow += month.height;
|
||||
}
|
||||
|
||||
let cursorAbove = this.anchorPlaneTop;
|
||||
for (let i = this.anchorMonthIndex - 1; i >= 0; i--) {
|
||||
const month = this.months[i];
|
||||
cursorAbove -= month.height;
|
||||
month.planeTop = cursorAbove;
|
||||
}
|
||||
|
||||
const lastMonth = this.months.at(-1)!;
|
||||
const contentBottom = lastMonth.planeTop + lastMonth.height + this.bottomSectionHeight;
|
||||
this.planeHeight = Math.min(VirtualScrollManager.PLANE_SIZE, contentBottom);
|
||||
}
|
||||
|
||||
/** Soft repoint: change the anchor month without moving any planeTop or scrollTop. */
|
||||
trackAnchorToViewportTop() {
|
||||
if (this.months.length === 0) {
|
||||
return;
|
||||
}
|
||||
const visibleTop = this.visibleWindow.top;
|
||||
let newAnchorIndex = -1;
|
||||
for (let i = 0; i < this.months.length; i++) {
|
||||
const month = this.months[i];
|
||||
if (month.planeTop + month.height > visibleTop) {
|
||||
newAnchorIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (newAnchorIndex === -1 || newAnchorIndex === this.anchorMonthIndex) {
|
||||
return;
|
||||
}
|
||||
this.anchorMonthIndex = newAnchorIndex;
|
||||
this.anchorPlaneTop = this.months[newAnchorIndex].planeTop;
|
||||
}
|
||||
|
||||
// Each scroll event resets this timer, so a brief pause in scrolling recenters
|
||||
// the plane. Continuous scrolling near a plane edge bypasses it via isNearPlaneEdge.
|
||||
static readonly RECENTER_DEBOUNCE_MS = 50;
|
||||
static readonly PLANE_EDGE_THRESHOLD = 50_000;
|
||||
|
||||
#scheduleRecenter() {
|
||||
clearTimeout(this.#recenterTimer);
|
||||
this.#recenterTimer = setTimeout(() => {
|
||||
this.#recenterTimer = undefined;
|
||||
this.recenterPlane();
|
||||
}, TimelineManager.RECENTER_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
isNearPlaneEdge(): boolean {
|
||||
return (
|
||||
this.scrollTop < TimelineManager.PLANE_EDGE_THRESHOLD ||
|
||||
this.scrollTop > VirtualScrollManager.PLANE_SIZE - TimelineManager.PLANE_EDGE_THRESHOLD
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hard repoint: slide every planeTop and scrollTop to pull the anchor back
|
||||
* toward PLANE_CENTER, or pin month 0 to topSectionHeight when it fits.
|
||||
*/
|
||||
recenterPlane() {
|
||||
clearTimeout(this.#recenterTimer);
|
||||
this.#recenterTimer = undefined;
|
||||
if (this.#recentering || this.months.length === 0 || this.anchorMonthIndex === -1) {
|
||||
return;
|
||||
}
|
||||
const viewportTopMonth = this.months[this.anchorMonthIndex];
|
||||
if (!viewportTopMonth) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pin months[0] when the visible content still fits on the plane alongside it.
|
||||
// Only the downward distance is checked because nothing exists above month 0.
|
||||
// Fall back to PLANE_CENTER recycling only when month 0 no longer fits.
|
||||
const firstMonth = this.months[0];
|
||||
const viewportTopOffsetFromFirstMonth = viewportTopMonth.planeTop - firstMonth.planeTop + this.topSectionHeight;
|
||||
const canPinFirstMonth =
|
||||
viewportTopOffsetFromFirstMonth + this.viewportHeight <=
|
||||
VirtualScrollManager.PLANE_SIZE - TimelineManager.PLANE_EDGE_THRESHOLD;
|
||||
|
||||
let targetMonth: TimelineMonth;
|
||||
let targetPlaneTop: number;
|
||||
if (canPinFirstMonth || this.anchorMonthIndex === 0) {
|
||||
targetMonth = firstMonth;
|
||||
targetPlaneTop = this.topSectionHeight;
|
||||
} else {
|
||||
targetMonth = viewportTopMonth;
|
||||
targetPlaneTop = VirtualScrollManager.PLANE_CENTER;
|
||||
}
|
||||
const monthIndex = this.months.indexOf(targetMonth);
|
||||
const delta = targetPlaneTop - targetMonth.planeTop;
|
||||
if (delta === 0) {
|
||||
return;
|
||||
}
|
||||
// Same lead-in guard as onTopSectionHeightChanged.
|
||||
const preserveScrollTop = this.scrollTop <= this.topSectionHeight;
|
||||
this.#recentering = true;
|
||||
try {
|
||||
for (const month of this.months) {
|
||||
month.planeTop += delta;
|
||||
}
|
||||
this.anchorMonthIndex = monthIndex;
|
||||
this.anchorPlaneTop = targetPlaneTop;
|
||||
if (this.#scrollableElement && !preserveScrollTop) {
|
||||
this.#scrollableElement.scrollTop += delta;
|
||||
}
|
||||
const lastMonth = this.months.at(-1)!;
|
||||
const contentBottom = lastMonth.planeTop + lastMonth.height + this.bottomSectionHeight;
|
||||
this.planeHeight = Math.min(VirtualScrollManager.PLANE_SIZE, contentBottom);
|
||||
this.updateSlidingWindow();
|
||||
} finally {
|
||||
this.#recentering = false;
|
||||
}
|
||||
}
|
||||
|
||||
override updateSlidingWindow() {
|
||||
super.updateSlidingWindow();
|
||||
if (this.#recentering || this.#scrubbing) {
|
||||
return;
|
||||
}
|
||||
this.trackAnchorToViewportTop();
|
||||
|
||||
// Continuous scroll keeps resetting the debounce timer, so if scrollTop is
|
||||
// already near a plane edge we have to recenter immediately or risk hitting it.
|
||||
if (this.isNearPlaneEdge()) {
|
||||
this.recenterPlane();
|
||||
return;
|
||||
}
|
||||
this.#scheduleRecenter();
|
||||
}
|
||||
|
||||
setScrubbing(value: boolean) {
|
||||
this.#scrubbing = value;
|
||||
if (!value) {
|
||||
this.updateSlidingWindow();
|
||||
}
|
||||
}
|
||||
|
||||
jumpToMonth({ monthIndex, fractionInMonth }: { monthIndex: number; fractionInMonth: number }) {
|
||||
clearTimeout(this.#recenterTimer);
|
||||
this.#recenterTimer = undefined;
|
||||
if (this.months.length === 0) {
|
||||
return;
|
||||
}
|
||||
const month = this.months[monthIndex];
|
||||
if (!month) {
|
||||
return;
|
||||
}
|
||||
this.anchorMonthIndex = monthIndex;
|
||||
this.anchorPlaneTop = monthIndex === 0 ? this.topSectionHeight : VirtualScrollManager.PLANE_CENTER;
|
||||
this.positionMonthsOnPlane();
|
||||
this.scrollTo(month.planeTop + fractionInMonth * month.height);
|
||||
}
|
||||
|
||||
async #initializeTimelineMonths() {
|
||||
const timebuckets = await getTimeBuckets({
|
||||
...authManager.params,
|
||||
@@ -280,6 +475,7 @@ export class TimelineManager extends VirtualScrollManager {
|
||||
async #init(options: TimelineManagerOptions) {
|
||||
this.isInitialized = false;
|
||||
this.months = [];
|
||||
this.anchorMonthIndex = -1;
|
||||
this.albumAssets.clear();
|
||||
await this.initTask.execute(async () => {
|
||||
this.#options = options;
|
||||
@@ -324,6 +520,13 @@ export class TimelineManager extends VirtualScrollManager {
|
||||
for (const month of this.months) {
|
||||
updateGeometry(this, month, { invalidateHeight: changedWidth });
|
||||
}
|
||||
|
||||
if (this.months.length > 0 && this.anchorMonthIndex === -1) {
|
||||
this.anchorMonthIndex = 0;
|
||||
this.anchorPlaneTop = this.topSectionHeight;
|
||||
}
|
||||
this.positionMonthsOnPlane();
|
||||
|
||||
this.updateViewportProximities();
|
||||
if (changedWidth) {
|
||||
this.#createScrubberMonths();
|
||||
@@ -336,9 +539,7 @@ export class TimelineManager extends VirtualScrollManager {
|
||||
year: month.yearMonth.year,
|
||||
month: month.yearMonth.month,
|
||||
title: month.title,
|
||||
height: month.height,
|
||||
}));
|
||||
this.scrubberTimelineHeight = this.totalViewerHeight;
|
||||
}
|
||||
|
||||
async loadTimelineMonth(yearMonth: TimelineYearMonth, options?: { cancelable: boolean }): Promise<void> {
|
||||
|
||||
@@ -36,7 +36,7 @@ export class TimelineMonth {
|
||||
readonly timelineManager: TimelineManager;
|
||||
|
||||
#height: number = $state(0);
|
||||
#top: number = $state(0);
|
||||
#planeTop: number = $state(0);
|
||||
|
||||
#initialCount: number = 0;
|
||||
#sortOrder: AssetOrder = AssetOrder.Desc;
|
||||
@@ -266,39 +266,36 @@ export class TimelineMonth {
|
||||
return;
|
||||
}
|
||||
const timelineManager = this.timelineManager;
|
||||
const index = timelineManager.months.indexOf(this);
|
||||
// Repin the anchor BEFORE positionMonthsOnPlane re-derives planeTops, so the
|
||||
// recomputation only shifts content above the viewport (invisible to the user).
|
||||
timelineManager.trackAnchorToViewportTop();
|
||||
// When this month is the viewport-top one, its photos will reflow as the height
|
||||
// settles from estimate to actual; capture the user's fractional position so we
|
||||
// can restore it below and avoid the visible stutter.
|
||||
const isViewportTopMonth =
|
||||
timelineManager.anchorMonthIndex !== -1 &&
|
||||
timelineManager.months[timelineManager.anchorMonthIndex] === this &&
|
||||
this.#height > 0;
|
||||
const scrollFractionInMonth = isViewportTopMonth ? (timelineManager.scrollTop - this.#planeTop) / this.#height : 0;
|
||||
const heightDelta = height - this.#height;
|
||||
this.#height = height;
|
||||
const previousTimelineMonth = timelineManager.months[index - 1];
|
||||
if (previousTimelineMonth) {
|
||||
const newTop = previousTimelineMonth.#top + previousTimelineMonth.#height;
|
||||
if (this.#top !== newTop) {
|
||||
this.#top = newTop;
|
||||
}
|
||||
}
|
||||
|
||||
if (heightDelta === 0) {
|
||||
return;
|
||||
}
|
||||
for (let cursor = index + 1; cursor < timelineManager.months.length; cursor++) {
|
||||
const timelineMonth = this.timelineManager.months[cursor];
|
||||
const newTop = timelineMonth.#top + heightDelta;
|
||||
if (timelineMonth.#top !== newTop) {
|
||||
timelineMonth.#top = newTop;
|
||||
}
|
||||
|
||||
// Shift the anchor instead of scrollTop — touching scrollTop here fights
|
||||
// native scroll momentum on Safari and visibly stutters.
|
||||
if (isViewportTopMonth && scrollFractionInMonth > 0) {
|
||||
timelineManager.anchorPlaneTop -= heightDelta * scrollFractionInMonth;
|
||||
}
|
||||
if (!timelineManager.viewportTopMonthIntersection) {
|
||||
return;
|
||||
}
|
||||
const { month, monthBottomViewportRatio, viewportTopRatioInMonth } = timelineManager.viewportTopMonthIntersection;
|
||||
const currentIndex = month ? timelineManager.months.indexOf(month) : -1;
|
||||
if (!month || currentIndex <= 0 || index > currentIndex) {
|
||||
return;
|
||||
}
|
||||
if (index < currentIndex || monthBottomViewportRatio < 1) {
|
||||
timelineManager.scrollBy(heightDelta);
|
||||
} else if (index === currentIndex) {
|
||||
const scrollTo = this.top + height * viewportTopRatioInMonth;
|
||||
timelineManager.scrollTo(scrollTo);
|
||||
|
||||
timelineManager.positionMonthsOnPlane();
|
||||
|
||||
// Async loads change heights without going through updateSlidingWindow, so the
|
||||
// near-edge check needs to run here too.
|
||||
if (timelineManager.isNearPlaneEdge()) {
|
||||
timelineManager.recenterPlane();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,8 +303,12 @@ export class TimelineMonth {
|
||||
return this.#height;
|
||||
}
|
||||
|
||||
get top(): number {
|
||||
return this.#top + this.timelineManager.topSectionHeight;
|
||||
get planeTop(): number {
|
||||
return this.#planeTop;
|
||||
}
|
||||
|
||||
set planeTop(value: number) {
|
||||
this.#planeTop = value;
|
||||
}
|
||||
|
||||
#handleLoadError(error: unknown) {
|
||||
@@ -337,7 +338,7 @@ export class TimelineMonth {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
top: this.top + group.top + viewerAsset.position.top + this.timelineManager.headerHeight,
|
||||
top: this.planeTop + group.top + viewerAsset.position.top + this.timelineManager.headerHeight,
|
||||
height: viewerAsset.position.height,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -79,7 +79,6 @@ export interface UpdateStackAssets {
|
||||
export type PendingChange = AddAsset | UpdateAsset | DeleteAsset | TrashAssets | UpdateStackAssets;
|
||||
|
||||
export type ScrubberMonth = {
|
||||
height: number;
|
||||
assetCount: number;
|
||||
year: number;
|
||||
month: number;
|
||||
|
||||
Reference in New Issue
Block a user