mirror of
https://github.com/immich-app/immich.git
synced 2025-12-22 17:24:56 +03:00
merge main
This commit is contained in:
@@ -41,7 +41,7 @@ By default, Immich will keep the last 14 database dumps and create a new dump ev
|
|||||||
|
|
||||||
#### Trigger Dump
|
#### Trigger Dump
|
||||||
|
|
||||||
You are able to trigger a database dump in the [admin job status page](http://my.immich.app/admin/jobs-status).
|
You are able to trigger a database dump in the [admin job status page](http://my.immich.app/admin/queues).
|
||||||
Visit the page, open the "Create job" modal from the top right, select "Create Database Dump" and click "Confirm".
|
Visit the page, open the "Create job" modal from the top right, select "Create Database Dump" and click "Confirm".
|
||||||
A job will run and trigger a dump, you can verify this worked correctly by checking the logs or the `backups/` folder.
|
A job will run and trigger a dump, you can verify this worked correctly by checking the logs or the `backups/` folder.
|
||||||
This dumps will count towards the last `X` dumps that will be kept based on your settings.
|
This dumps will count towards the last `X` dumps that will be kept based on your settings.
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ server {
|
|||||||
# allow large file uploads
|
# allow large file uploads
|
||||||
client_max_body_size 50000M;
|
client_max_body_size 50000M;
|
||||||
|
|
||||||
|
# disable buffering uploads to prevent OOM on reverse proxy server and make uploads twice as fast (no pause)
|
||||||
|
proxy_request_buffering off;
|
||||||
|
|
||||||
# Set headers
|
# Set headers
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ You can access the web from `http://your-machine-ip:3000` or `http://localhost:3
|
|||||||
**Notes:**
|
**Notes:**
|
||||||
|
|
||||||
- The "web" development container runs with uid 1000. If that uid does not have read/write permissions on the mounted volumes, you may encounter errors
|
- The "web" development container runs with uid 1000. If that uid does not have read/write permissions on the mounted volumes, you may encounter errors
|
||||||
- In case of rootless docker setup, you need to use root within the container, otherwise you will encounter read/write permission related errors, see comments in `docker/docker-compose.dev.yml`.
|
|
||||||
|
|
||||||
#### Connect web to a remote backend
|
#### Connect web to a remote backend
|
||||||
|
|
||||||
|
|||||||
@@ -1222,4 +1222,4 @@ Feel free to make a feature request if there's a model you want to use that we d
|
|||||||
[huggingface-clip]: https://huggingface.co/collections/immich-app/clip-654eaefb077425890874cd07
|
[huggingface-clip]: https://huggingface.co/collections/immich-app/clip-654eaefb077425890874cd07
|
||||||
[huggingface-multilingual-clip]: https://huggingface.co/collections/immich-app/multilingual-clip-654eb08c2382f591eeb8c2a7
|
[huggingface-multilingual-clip]: https://huggingface.co/collections/immich-app/multilingual-clip-654eb08c2382f591eeb8c2a7
|
||||||
[smart-search-settings]: https://my.immich.app/admin/system-settings?isOpen=machine-learning+smart-search
|
[smart-search-settings]: https://my.immich.app/admin/system-settings?isOpen=machine-learning+smart-search
|
||||||
[job-status-page]: https://my.immich.app/admin/jobs-status
|
[job-status-page]: https://my.immich.app/admin/queues
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ Version mismatches between both hosts may cause bugs and instability, so remembe
|
|||||||
|
|
||||||
Adding a new URL to the settings is recommended over replacing the existing URL. This is because it will allow machine learning tasks to be processed successfully when the remote server is down by falling back to the local machine learning container. If you do not want machine learning tasks to be processed locally when the remote server is not available, you can instead replace the existing URL and only provide the remote container's URL. If doing this, you can remove the `immich-machine-learning` section of the local `docker-compose.yml` file to save resources, as this service will never be used.
|
Adding a new URL to the settings is recommended over replacing the existing URL. This is because it will allow machine learning tasks to be processed successfully when the remote server is down by falling back to the local machine learning container. If you do not want machine learning tasks to be processed locally when the remote server is not available, you can instead replace the existing URL and only provide the remote container's URL. If doing this, you can remove the `immich-machine-learning` section of the local `docker-compose.yml` file to save resources, as this service will never be used.
|
||||||
|
|
||||||
Do note that this will mean that Smart Search and Face Detection jobs will fail to be processed when the remote instance is not available. This in turn means that tasks dependent on these features—Duplicate Detection and Facial Recognition—will not run for affected assets. If this occurs, you must manually click the _Missing_ button next to Smart Search and Face Detection in the [Job Status](http://my.immich.app/admin/jobs-status) page for the jobs to be retried.
|
Do note that this will mean that Smart Search and Face Detection jobs will fail to be processed when the remote instance is not available. This in turn means that tasks dependent on these features—Duplicate Detection and Facial Recognition—will not run for affected assets. If this occurs, you must manually click the _Missing_ button next to Smart Search and Face Detection in the [Job Status](http://my.immich.app/admin/queues) page for the jobs to be retried.
|
||||||
|
|
||||||
## Load balancing
|
## Load balancing
|
||||||
|
|
||||||
|
|||||||
10
i18n/en.json
10
i18n/en.json
@@ -8,6 +8,7 @@
|
|||||||
"action_description": "A set of action to perform on the filtered assets",
|
"action_description": "A set of action to perform on the filtered assets",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"active": "Active",
|
"active": "Active",
|
||||||
|
"active_count": "Active: {count}",
|
||||||
"activity": "Activity",
|
"activity": "Activity",
|
||||||
"activity_changed": "Activity is {enabled, select, true {enabled} other {disabled}}",
|
"activity_changed": "Activity is {enabled, select, true {enabled} other {disabled}}",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
@@ -117,10 +118,9 @@
|
|||||||
"job_not_concurrency_safe": "This job is not concurrency-safe.",
|
"job_not_concurrency_safe": "This job is not concurrency-safe.",
|
||||||
"job_settings": "Job Settings",
|
"job_settings": "Job Settings",
|
||||||
"job_settings_description": "Manage job concurrency",
|
"job_settings_description": "Manage job concurrency",
|
||||||
"job_status": "Job Status",
|
|
||||||
"jobs_delayed": "{jobCount, plural, other {# delayed}}",
|
"jobs_delayed": "{jobCount, plural, other {# delayed}}",
|
||||||
"jobs_failed": "{jobCount, plural, other {# failed}}",
|
"jobs_failed": "{jobCount, plural, other {# failed}}",
|
||||||
"jobs_page_description": "Admin jobs page",
|
"jobs_over_time": "Jobs over time",
|
||||||
"library_created": "Created library: {library}",
|
"library_created": "Created library: {library}",
|
||||||
"library_deleted": "Library deleted",
|
"library_deleted": "Library deleted",
|
||||||
"library_details": "Library details",
|
"library_details": "Library details",
|
||||||
@@ -283,10 +283,14 @@
|
|||||||
"password_settings_description": "Manage password login settings",
|
"password_settings_description": "Manage password login settings",
|
||||||
"paths_validated_successfully": "All paths validated successfully",
|
"paths_validated_successfully": "All paths validated successfully",
|
||||||
"person_cleanup_job": "Person cleanup",
|
"person_cleanup_job": "Person cleanup",
|
||||||
|
"queue_details": "Queue Details",
|
||||||
|
"queues": "Job Queues",
|
||||||
|
"queues_page_description": "Admin job queues page",
|
||||||
"quota_size_gib": "Quota Size (GiB)",
|
"quota_size_gib": "Quota Size (GiB)",
|
||||||
"refreshing_all_libraries": "Refreshing all libraries",
|
"refreshing_all_libraries": "Refreshing all libraries",
|
||||||
"registration": "Admin Registration",
|
"registration": "Admin Registration",
|
||||||
"registration_description": "Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative tasks, and additional users will be created by you.",
|
"registration_description": "Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative tasks, and additional users will be created by you.",
|
||||||
|
"remove_failed_jobs": "Remove failed jobs",
|
||||||
"require_password_change_on_login": "Require user to change password on first login",
|
"require_password_change_on_login": "Require user to change password on first login",
|
||||||
"reset_settings_to_default": "Reset settings to default",
|
"reset_settings_to_default": "Reset settings to default",
|
||||||
"reset_settings_to_recent_saved": "Reset settings to the recent saved settings",
|
"reset_settings_to_recent_saved": "Reset settings to the recent saved settings",
|
||||||
@@ -1122,6 +1126,7 @@
|
|||||||
"external_network_sheet_info": "When not on the preferred Wi-Fi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
|
"external_network_sheet_info": "When not on the preferred Wi-Fi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
|
||||||
"face_unassigned": "Unassigned",
|
"face_unassigned": "Unassigned",
|
||||||
"failed": "Failed",
|
"failed": "Failed",
|
||||||
|
"failed_count": "Failed: {count}",
|
||||||
"failed_to_authenticate": "Failed to authenticate",
|
"failed_to_authenticate": "Failed to authenticate",
|
||||||
"failed_to_load_assets": "Failed to load assets",
|
"failed_to_load_assets": "Failed to load assets",
|
||||||
"failed_to_load_folder": "Failed to load folder",
|
"failed_to_load_folder": "Failed to load folder",
|
||||||
@@ -2263,6 +2268,7 @@
|
|||||||
"visual": "Visual",
|
"visual": "Visual",
|
||||||
"visual_builder": "Visual builder",
|
"visual_builder": "Visual builder",
|
||||||
"waiting": "Waiting",
|
"waiting": "Waiting",
|
||||||
|
"waiting_count": "Waiting: {count}",
|
||||||
"warning": "Warning",
|
"warning": "Warning",
|
||||||
"week": "Week",
|
"week": "Week",
|
||||||
"welcome": "Welcome",
|
"welcome": "Welcome",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ experimental_monorepo_root = true
|
|||||||
[tools]
|
[tools]
|
||||||
node = "24.11.1"
|
node = "24.11.1"
|
||||||
flutter = "3.35.7"
|
flutter = "3.35.7"
|
||||||
pnpm = "10.22.0"
|
pnpm = "10.24.0"
|
||||||
terragrunt = "0.93.10"
|
terragrunt = "0.93.10"
|
||||||
opentofu = "1.10.7"
|
opentofu = "1.10.7"
|
||||||
java = "25.0.1"
|
java = "25.0.1"
|
||||||
|
|||||||
@@ -89,7 +89,10 @@ data class PlatformAsset (
|
|||||||
val height: Long? = null,
|
val height: Long? = null,
|
||||||
val durationInSeconds: Long,
|
val durationInSeconds: Long,
|
||||||
val orientation: Long,
|
val orientation: Long,
|
||||||
val isFavorite: Boolean
|
val isFavorite: Boolean,
|
||||||
|
val adjustmentTime: Long? = null,
|
||||||
|
val latitude: Double? = null,
|
||||||
|
val longitude: Double? = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
companion object {
|
companion object {
|
||||||
@@ -104,7 +107,10 @@ data class PlatformAsset (
|
|||||||
val durationInSeconds = pigeonVar_list[7] as Long
|
val durationInSeconds = pigeonVar_list[7] as Long
|
||||||
val orientation = pigeonVar_list[8] as Long
|
val orientation = pigeonVar_list[8] as Long
|
||||||
val isFavorite = pigeonVar_list[9] as Boolean
|
val isFavorite = pigeonVar_list[9] as Boolean
|
||||||
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite)
|
val adjustmentTime = pigeonVar_list[10] as Long?
|
||||||
|
val latitude = pigeonVar_list[11] as Double?
|
||||||
|
val longitude = pigeonVar_list[12] as Double?
|
||||||
|
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite, adjustmentTime, latitude, longitude)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun toList(): List<Any?> {
|
fun toList(): List<Any?> {
|
||||||
@@ -119,6 +125,9 @@ data class PlatformAsset (
|
|||||||
durationInSeconds,
|
durationInSeconds,
|
||||||
orientation,
|
orientation,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
|
adjustmentTime,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
|
|||||||
1
mobile/drift_schemas/main/drift_schema_v14.json
generated
Normal file
1
mobile/drift_schemas/main/drift_schema_v14.json
generated
Normal file
File diff suppressed because one or more lines are too long
@@ -140,6 +140,9 @@ struct PlatformAsset: Hashable {
|
|||||||
var durationInSeconds: Int64
|
var durationInSeconds: Int64
|
||||||
var orientation: Int64
|
var orientation: Int64
|
||||||
var isFavorite: Bool
|
var isFavorite: Bool
|
||||||
|
var adjustmentTime: Int64? = nil
|
||||||
|
var latitude: Double? = nil
|
||||||
|
var longitude: Double? = nil
|
||||||
|
|
||||||
|
|
||||||
// swift-format-ignore: AlwaysUseLowerCamelCase
|
// swift-format-ignore: AlwaysUseLowerCamelCase
|
||||||
@@ -154,6 +157,9 @@ struct PlatformAsset: Hashable {
|
|||||||
let durationInSeconds = pigeonVar_list[7] as! Int64
|
let durationInSeconds = pigeonVar_list[7] as! Int64
|
||||||
let orientation = pigeonVar_list[8] as! Int64
|
let orientation = pigeonVar_list[8] as! Int64
|
||||||
let isFavorite = pigeonVar_list[9] as! Bool
|
let isFavorite = pigeonVar_list[9] as! Bool
|
||||||
|
let adjustmentTime: Int64? = nilOrValue(pigeonVar_list[10])
|
||||||
|
let latitude: Double? = nilOrValue(pigeonVar_list[11])
|
||||||
|
let longitude: Double? = nilOrValue(pigeonVar_list[12])
|
||||||
|
|
||||||
return PlatformAsset(
|
return PlatformAsset(
|
||||||
id: id,
|
id: id,
|
||||||
@@ -165,7 +171,10 @@ struct PlatformAsset: Hashable {
|
|||||||
height: height,
|
height: height,
|
||||||
durationInSeconds: durationInSeconds,
|
durationInSeconds: durationInSeconds,
|
||||||
orientation: orientation,
|
orientation: orientation,
|
||||||
isFavorite: isFavorite
|
isFavorite: isFavorite,
|
||||||
|
adjustmentTime: adjustmentTime,
|
||||||
|
latitude: latitude,
|
||||||
|
longitude: longitude
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
func toList() -> [Any?] {
|
func toList() -> [Any?] {
|
||||||
@@ -180,6 +189,9 @@ struct PlatformAsset: Hashable {
|
|||||||
durationInSeconds,
|
durationInSeconds,
|
||||||
orientation,
|
orientation,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
|
adjustmentTime,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
|
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ extension PHAsset {
|
|||||||
height: Int64(pixelHeight),
|
height: Int64(pixelHeight),
|
||||||
durationInSeconds: Int64(duration),
|
durationInSeconds: Int64(duration),
|
||||||
orientation: 0,
|
orientation: 0,
|
||||||
isFavorite: isFavorite
|
isFavorite: isFavorite,
|
||||||
|
adjustmentTime: adjustmentTimestamp,
|
||||||
|
latitude: location?.coordinate.latitude,
|
||||||
|
longitude: location?.coordinate.longitude
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,6 +27,13 @@ extension PHAsset {
|
|||||||
return value(forKey: "filename") as? String
|
return value(forKey: "filename") as? String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var adjustmentTimestamp: Int64? {
|
||||||
|
if let date = value(forKey: "adjustmentTimestamp") as? Date {
|
||||||
|
return Int64(date.timeIntervalSince1970)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// This method is expected to be slow as it goes through the asset resources to fetch the originalFilename
|
// This method is expected to be slow as it goes through the asset resources to fetch the originalFilename
|
||||||
var originalFilename: String? {
|
var originalFilename: String? {
|
||||||
return getResource()?.originalFilename
|
return getResource()?.originalFilename
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ class LocalAsset extends BaseAsset {
|
|||||||
final String? remoteAssetId;
|
final String? remoteAssetId;
|
||||||
final int orientation;
|
final int orientation;
|
||||||
|
|
||||||
|
final DateTime? adjustmentTime;
|
||||||
|
final double? latitude;
|
||||||
|
final double? longitude;
|
||||||
|
|
||||||
const LocalAsset({
|
const LocalAsset({
|
||||||
required this.id,
|
required this.id,
|
||||||
String? remoteId,
|
String? remoteId,
|
||||||
@@ -19,6 +23,9 @@ class LocalAsset extends BaseAsset {
|
|||||||
super.isFavorite = false,
|
super.isFavorite = false,
|
||||||
super.livePhotoVideoId,
|
super.livePhotoVideoId,
|
||||||
this.orientation = 0,
|
this.orientation = 0,
|
||||||
|
this.adjustmentTime,
|
||||||
|
this.latitude,
|
||||||
|
this.longitude,
|
||||||
}) : remoteAssetId = remoteId;
|
}) : remoteAssetId = remoteId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -33,6 +40,8 @@ class LocalAsset extends BaseAsset {
|
|||||||
@override
|
@override
|
||||||
String get heroTag => '${id}_${remoteId ?? checksum}';
|
String get heroTag => '${id}_${remoteId ?? checksum}';
|
||||||
|
|
||||||
|
bool get hasCoordinates => latitude != null && longitude != null && latitude != 0 && longitude != 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '''LocalAsset {
|
return '''LocalAsset {
|
||||||
@@ -47,6 +56,9 @@ class LocalAsset extends BaseAsset {
|
|||||||
remoteId: ${remoteId ?? "<NA>"}
|
remoteId: ${remoteId ?? "<NA>"}
|
||||||
isFavorite: $isFavorite,
|
isFavorite: $isFavorite,
|
||||||
orientation: $orientation,
|
orientation: $orientation,
|
||||||
|
adjustmentTime: $adjustmentTime,
|
||||||
|
latitude: ${latitude ?? "<NA>"},
|
||||||
|
longitude: ${longitude ?? "<NA>"},
|
||||||
}''';
|
}''';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,11 +67,23 @@ class LocalAsset extends BaseAsset {
|
|||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (other is! LocalAsset) return false;
|
if (other is! LocalAsset) return false;
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
return super == other && id == other.id && orientation == other.orientation;
|
return super == other &&
|
||||||
|
id == other.id &&
|
||||||
|
orientation == other.orientation &&
|
||||||
|
adjustmentTime == other.adjustmentTime &&
|
||||||
|
latitude == other.latitude &&
|
||||||
|
longitude == other.longitude;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => super.hashCode ^ id.hashCode ^ remoteId.hashCode ^ orientation.hashCode;
|
int get hashCode =>
|
||||||
|
super.hashCode ^
|
||||||
|
id.hashCode ^
|
||||||
|
remoteId.hashCode ^
|
||||||
|
orientation.hashCode ^
|
||||||
|
adjustmentTime.hashCode ^
|
||||||
|
latitude.hashCode ^
|
||||||
|
longitude.hashCode;
|
||||||
|
|
||||||
LocalAsset copyWith({
|
LocalAsset copyWith({
|
||||||
String? id,
|
String? id,
|
||||||
@@ -74,6 +98,9 @@ class LocalAsset extends BaseAsset {
|
|||||||
int? durationInSeconds,
|
int? durationInSeconds,
|
||||||
bool? isFavorite,
|
bool? isFavorite,
|
||||||
int? orientation,
|
int? orientation,
|
||||||
|
DateTime? adjustmentTime,
|
||||||
|
double? latitude,
|
||||||
|
double? longitude,
|
||||||
}) {
|
}) {
|
||||||
return LocalAsset(
|
return LocalAsset(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -88,6 +115,9 @@ class LocalAsset extends BaseAsset {
|
|||||||
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
|
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
|
||||||
isFavorite: isFavorite ?? this.isFavorite,
|
isFavorite: isFavorite ?? this.isFavorite,
|
||||||
orientation: orientation ?? this.orientation,
|
orientation: orientation ?? this.orientation,
|
||||||
|
adjustmentTime: adjustmentTime ?? this.adjustmentTime,
|
||||||
|
latitude: latitude ?? this.latitude,
|
||||||
|
longitude: longitude ?? this.longitude,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class ExifInfo {
|
|||||||
|
|
||||||
String get fNumber => f == null ? "" : f!.toStringAsFixed(1);
|
String get fNumber => f == null ? "" : f!.toStringAsFixed(1);
|
||||||
|
|
||||||
String get focalLength => mm == null ? "" : mm!.toStringAsFixed(1);
|
String get focalLength => mm == null ? "" : mm!.toStringAsFixed(3);
|
||||||
|
|
||||||
const ExifInfo({
|
const ExifInfo({
|
||||||
this.assetId,
|
this.assetId,
|
||||||
|
|||||||
@@ -286,6 +286,7 @@ class LocalSyncService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool _assetsEqual(LocalAsset a, LocalAsset b) {
|
bool _assetsEqual(LocalAsset a, LocalAsset b) {
|
||||||
|
if (CurrentPlatform.isAndroid) {
|
||||||
return a.updatedAt.isAtSameMomentAs(b.updatedAt) &&
|
return a.updatedAt.isAtSameMomentAs(b.updatedAt) &&
|
||||||
a.createdAt.isAtSameMomentAs(b.createdAt) &&
|
a.createdAt.isAtSameMomentAs(b.createdAt) &&
|
||||||
a.width == b.width &&
|
a.width == b.width &&
|
||||||
@@ -293,6 +294,17 @@ class LocalSyncService {
|
|||||||
a.durationInSeconds == b.durationInSeconds;
|
a.durationInSeconds == b.durationInSeconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final firstAdjustment = a.adjustmentTime?.millisecondsSinceEpoch ?? 0;
|
||||||
|
final secondAdjustment = b.adjustmentTime?.millisecondsSinceEpoch ?? 0;
|
||||||
|
return firstAdjustment == secondAdjustment &&
|
||||||
|
a.createdAt.isAtSameMomentAs(b.createdAt) &&
|
||||||
|
a.width == b.width &&
|
||||||
|
a.height == b.height &&
|
||||||
|
a.durationInSeconds == b.durationInSeconds &&
|
||||||
|
a.latitude == b.latitude &&
|
||||||
|
a.longitude == b.longitude;
|
||||||
|
}
|
||||||
|
|
||||||
bool _albumsEqual(LocalAlbum a, LocalAlbum b) {
|
bool _albumsEqual(LocalAlbum a, LocalAlbum b) {
|
||||||
return a.name == b.name && a.assetCount == b.assetCount && a.updatedAt.isAtSameMomentAs(b.updatedAt);
|
return a.name == b.name && a.assetCount == b.assetCount && a.updatedAt.isAtSameMomentAs(b.updatedAt);
|
||||||
}
|
}
|
||||||
@@ -376,5 +388,8 @@ extension PlatformToLocalAsset on PlatformAsset {
|
|||||||
durationInSeconds: durationInSeconds,
|
durationInSeconds: durationInSeconds,
|
||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
orientation: orientation,
|
orientation: orientation,
|
||||||
|
adjustmentTime: tryFromSecondsSinceEpoch(adjustmentTime, isUtc: true),
|
||||||
|
latitude: latitude,
|
||||||
|
longitude: longitude,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,5 +166,6 @@ extension RemoteExifEntityDataDomainEx on RemoteExifEntityData {
|
|||||||
mm: focalLength?.toDouble(),
|
mm: focalLength?.toDouble(),
|
||||||
lens: lens,
|
lens: lens,
|
||||||
isFlipped: ExifDtoConverter.isOrientationFlipped(orientation),
|
isFlipped: ExifDtoConverter.isOrientationFlipped(orientation),
|
||||||
|
exposureSeconds: ExifDtoConverter.exposureTimeToSeconds(exposureTime),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
|||||||
|
|
||||||
IntColumn get orientation => integer().withDefault(const Constant(0))();
|
IntColumn get orientation => integer().withDefault(const Constant(0))();
|
||||||
|
|
||||||
|
DateTimeColumn get adjustmentTime => dateTime().nullable()();
|
||||||
|
|
||||||
|
RealColumn get latitude => real().nullable()();
|
||||||
|
|
||||||
|
RealColumn get longitude => real().nullable()();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Set<Column> get primaryKey => {id};
|
Set<Column> get primaryKey => {id};
|
||||||
}
|
}
|
||||||
@@ -34,5 +40,8 @@ extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData {
|
|||||||
width: width,
|
width: width,
|
||||||
remoteId: remoteId,
|
remoteId: remoteId,
|
||||||
orientation: orientation,
|
orientation: orientation,
|
||||||
|
adjustmentTime: adjustmentTime,
|
||||||
|
latitude: latitude,
|
||||||
|
longitude: longitude,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ typedef $$LocalAssetEntityTableCreateCompanionBuilder =
|
|||||||
i0.Value<String?> checksum,
|
i0.Value<String?> checksum,
|
||||||
i0.Value<bool> isFavorite,
|
i0.Value<bool> isFavorite,
|
||||||
i0.Value<int> orientation,
|
i0.Value<int> orientation,
|
||||||
|
i0.Value<DateTime?> adjustmentTime,
|
||||||
|
i0.Value<double?> latitude,
|
||||||
|
i0.Value<double?> longitude,
|
||||||
});
|
});
|
||||||
typedef $$LocalAssetEntityTableUpdateCompanionBuilder =
|
typedef $$LocalAssetEntityTableUpdateCompanionBuilder =
|
||||||
i1.LocalAssetEntityCompanion Function({
|
i1.LocalAssetEntityCompanion Function({
|
||||||
@@ -35,6 +38,9 @@ typedef $$LocalAssetEntityTableUpdateCompanionBuilder =
|
|||||||
i0.Value<String?> checksum,
|
i0.Value<String?> checksum,
|
||||||
i0.Value<bool> isFavorite,
|
i0.Value<bool> isFavorite,
|
||||||
i0.Value<int> orientation,
|
i0.Value<int> orientation,
|
||||||
|
i0.Value<DateTime?> adjustmentTime,
|
||||||
|
i0.Value<double?> latitude,
|
||||||
|
i0.Value<double?> longitude,
|
||||||
});
|
});
|
||||||
|
|
||||||
class $$LocalAssetEntityTableFilterComposer
|
class $$LocalAssetEntityTableFilterComposer
|
||||||
@@ -101,6 +107,21 @@ class $$LocalAssetEntityTableFilterComposer
|
|||||||
column: $table.orientation,
|
column: $table.orientation,
|
||||||
builder: (column) => i0.ColumnFilters(column),
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
i0.ColumnFilters<DateTime> get adjustmentTime => $composableBuilder(
|
||||||
|
column: $table.adjustmentTime,
|
||||||
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
i0.ColumnFilters<double> get latitude => $composableBuilder(
|
||||||
|
column: $table.latitude,
|
||||||
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
i0.ColumnFilters<double> get longitude => $composableBuilder(
|
||||||
|
column: $table.longitude,
|
||||||
|
builder: (column) => i0.ColumnFilters(column),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$LocalAssetEntityTableOrderingComposer
|
class $$LocalAssetEntityTableOrderingComposer
|
||||||
@@ -166,6 +187,21 @@ class $$LocalAssetEntityTableOrderingComposer
|
|||||||
column: $table.orientation,
|
column: $table.orientation,
|
||||||
builder: (column) => i0.ColumnOrderings(column),
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
i0.ColumnOrderings<DateTime> get adjustmentTime => $composableBuilder(
|
||||||
|
column: $table.adjustmentTime,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
i0.ColumnOrderings<double> get latitude => $composableBuilder(
|
||||||
|
column: $table.latitude,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
|
);
|
||||||
|
|
||||||
|
i0.ColumnOrderings<double> get longitude => $composableBuilder(
|
||||||
|
column: $table.longitude,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$LocalAssetEntityTableAnnotationComposer
|
class $$LocalAssetEntityTableAnnotationComposer
|
||||||
@@ -215,6 +251,17 @@ class $$LocalAssetEntityTableAnnotationComposer
|
|||||||
column: $table.orientation,
|
column: $table.orientation,
|
||||||
builder: (column) => column,
|
builder: (column) => column,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<DateTime> get adjustmentTime => $composableBuilder(
|
||||||
|
column: $table.adjustmentTime,
|
||||||
|
builder: (column) => column,
|
||||||
|
);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<double> get latitude =>
|
||||||
|
$composableBuilder(column: $table.latitude, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<double> get longitude =>
|
||||||
|
$composableBuilder(column: $table.longitude, builder: (column) => column);
|
||||||
}
|
}
|
||||||
|
|
||||||
class $$LocalAssetEntityTableTableManager
|
class $$LocalAssetEntityTableTableManager
|
||||||
@@ -268,6 +315,9 @@ class $$LocalAssetEntityTableTableManager
|
|||||||
i0.Value<String?> checksum = const i0.Value.absent(),
|
i0.Value<String?> checksum = const i0.Value.absent(),
|
||||||
i0.Value<bool> isFavorite = const i0.Value.absent(),
|
i0.Value<bool> isFavorite = const i0.Value.absent(),
|
||||||
i0.Value<int> orientation = const i0.Value.absent(),
|
i0.Value<int> orientation = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime?> adjustmentTime = const i0.Value.absent(),
|
||||||
|
i0.Value<double?> latitude = const i0.Value.absent(),
|
||||||
|
i0.Value<double?> longitude = const i0.Value.absent(),
|
||||||
}) => i1.LocalAssetEntityCompanion(
|
}) => i1.LocalAssetEntityCompanion(
|
||||||
name: name,
|
name: name,
|
||||||
type: type,
|
type: type,
|
||||||
@@ -280,6 +330,9 @@ class $$LocalAssetEntityTableTableManager
|
|||||||
checksum: checksum,
|
checksum: checksum,
|
||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
orientation: orientation,
|
orientation: orientation,
|
||||||
|
adjustmentTime: adjustmentTime,
|
||||||
|
latitude: latitude,
|
||||||
|
longitude: longitude,
|
||||||
),
|
),
|
||||||
createCompanionCallback:
|
createCompanionCallback:
|
||||||
({
|
({
|
||||||
@@ -294,6 +347,9 @@ class $$LocalAssetEntityTableTableManager
|
|||||||
i0.Value<String?> checksum = const i0.Value.absent(),
|
i0.Value<String?> checksum = const i0.Value.absent(),
|
||||||
i0.Value<bool> isFavorite = const i0.Value.absent(),
|
i0.Value<bool> isFavorite = const i0.Value.absent(),
|
||||||
i0.Value<int> orientation = const i0.Value.absent(),
|
i0.Value<int> orientation = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime?> adjustmentTime = const i0.Value.absent(),
|
||||||
|
i0.Value<double?> latitude = const i0.Value.absent(),
|
||||||
|
i0.Value<double?> longitude = const i0.Value.absent(),
|
||||||
}) => i1.LocalAssetEntityCompanion.insert(
|
}) => i1.LocalAssetEntityCompanion.insert(
|
||||||
name: name,
|
name: name,
|
||||||
type: type,
|
type: type,
|
||||||
@@ -306,6 +362,9 @@ class $$LocalAssetEntityTableTableManager
|
|||||||
checksum: checksum,
|
checksum: checksum,
|
||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
orientation: orientation,
|
orientation: orientation,
|
||||||
|
adjustmentTime: adjustmentTime,
|
||||||
|
latitude: latitude,
|
||||||
|
longitude: longitude,
|
||||||
),
|
),
|
||||||
withReferenceMapper: (p0) => p0
|
withReferenceMapper: (p0) => p0
|
||||||
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
|
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
|
||||||
@@ -473,6 +532,39 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
|
|||||||
requiredDuringInsert: false,
|
requiredDuringInsert: false,
|
||||||
defaultValue: const i4.Constant(0),
|
defaultValue: const i4.Constant(0),
|
||||||
);
|
);
|
||||||
|
static const i0.VerificationMeta _adjustmentTimeMeta =
|
||||||
|
const i0.VerificationMeta('adjustmentTime');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<DateTime> adjustmentTime =
|
||||||
|
i0.GeneratedColumn<DateTime>(
|
||||||
|
'adjustment_time',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i0.DriftSqlType.dateTime,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
);
|
||||||
|
static const i0.VerificationMeta _latitudeMeta = const i0.VerificationMeta(
|
||||||
|
'latitude',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<double> latitude = i0.GeneratedColumn<double>(
|
||||||
|
'latitude',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i0.DriftSqlType.double,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
);
|
||||||
|
static const i0.VerificationMeta _longitudeMeta = const i0.VerificationMeta(
|
||||||
|
'longitude',
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<double> longitude = i0.GeneratedColumn<double>(
|
||||||
|
'longitude',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i0.DriftSqlType.double,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
);
|
||||||
@override
|
@override
|
||||||
List<i0.GeneratedColumn> get $columns => [
|
List<i0.GeneratedColumn> get $columns => [
|
||||||
name,
|
name,
|
||||||
@@ -486,6 +578,9 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
|
|||||||
checksum,
|
checksum,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
orientation,
|
orientation,
|
||||||
|
adjustmentTime,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
String get aliasedName => _alias ?? actualTableName;
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
@@ -566,6 +661,27 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('adjustment_time')) {
|
||||||
|
context.handle(
|
||||||
|
_adjustmentTimeMeta,
|
||||||
|
adjustmentTime.isAcceptableOrUnknown(
|
||||||
|
data['adjustment_time']!,
|
||||||
|
_adjustmentTimeMeta,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data.containsKey('latitude')) {
|
||||||
|
context.handle(
|
||||||
|
_latitudeMeta,
|
||||||
|
latitude.isAcceptableOrUnknown(data['latitude']!, _latitudeMeta),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data.containsKey('longitude')) {
|
||||||
|
context.handle(
|
||||||
|
_longitudeMeta,
|
||||||
|
longitude.isAcceptableOrUnknown(data['longitude']!, _longitudeMeta),
|
||||||
|
);
|
||||||
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -624,6 +740,18 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
|
|||||||
i0.DriftSqlType.int,
|
i0.DriftSqlType.int,
|
||||||
data['${effectivePrefix}orientation'],
|
data['${effectivePrefix}orientation'],
|
||||||
)!,
|
)!,
|
||||||
|
adjustmentTime: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.dateTime,
|
||||||
|
data['${effectivePrefix}adjustment_time'],
|
||||||
|
),
|
||||||
|
latitude: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.double,
|
||||||
|
data['${effectivePrefix}latitude'],
|
||||||
|
),
|
||||||
|
longitude: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.double,
|
||||||
|
data['${effectivePrefix}longitude'],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -653,6 +781,9 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
final String? checksum;
|
final String? checksum;
|
||||||
final bool isFavorite;
|
final bool isFavorite;
|
||||||
final int orientation;
|
final int orientation;
|
||||||
|
final DateTime? adjustmentTime;
|
||||||
|
final double? latitude;
|
||||||
|
final double? longitude;
|
||||||
const LocalAssetEntityData({
|
const LocalAssetEntityData({
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.type,
|
required this.type,
|
||||||
@@ -665,6 +796,9 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
this.checksum,
|
this.checksum,
|
||||||
required this.isFavorite,
|
required this.isFavorite,
|
||||||
required this.orientation,
|
required this.orientation,
|
||||||
|
this.adjustmentTime,
|
||||||
|
this.latitude,
|
||||||
|
this.longitude,
|
||||||
});
|
});
|
||||||
@override
|
@override
|
||||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||||
@@ -692,6 +826,15 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
}
|
}
|
||||||
map['is_favorite'] = i0.Variable<bool>(isFavorite);
|
map['is_favorite'] = i0.Variable<bool>(isFavorite);
|
||||||
map['orientation'] = i0.Variable<int>(orientation);
|
map['orientation'] = i0.Variable<int>(orientation);
|
||||||
|
if (!nullToAbsent || adjustmentTime != null) {
|
||||||
|
map['adjustment_time'] = i0.Variable<DateTime>(adjustmentTime);
|
||||||
|
}
|
||||||
|
if (!nullToAbsent || latitude != null) {
|
||||||
|
map['latitude'] = i0.Variable<double>(latitude);
|
||||||
|
}
|
||||||
|
if (!nullToAbsent || longitude != null) {
|
||||||
|
map['longitude'] = i0.Variable<double>(longitude);
|
||||||
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -714,6 +857,9 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
checksum: serializer.fromJson<String?>(json['checksum']),
|
checksum: serializer.fromJson<String?>(json['checksum']),
|
||||||
isFavorite: serializer.fromJson<bool>(json['isFavorite']),
|
isFavorite: serializer.fromJson<bool>(json['isFavorite']),
|
||||||
orientation: serializer.fromJson<int>(json['orientation']),
|
orientation: serializer.fromJson<int>(json['orientation']),
|
||||||
|
adjustmentTime: serializer.fromJson<DateTime?>(json['adjustmentTime']),
|
||||||
|
latitude: serializer.fromJson<double?>(json['latitude']),
|
||||||
|
longitude: serializer.fromJson<double?>(json['longitude']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@override
|
@override
|
||||||
@@ -733,6 +879,9 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
'checksum': serializer.toJson<String?>(checksum),
|
'checksum': serializer.toJson<String?>(checksum),
|
||||||
'isFavorite': serializer.toJson<bool>(isFavorite),
|
'isFavorite': serializer.toJson<bool>(isFavorite),
|
||||||
'orientation': serializer.toJson<int>(orientation),
|
'orientation': serializer.toJson<int>(orientation),
|
||||||
|
'adjustmentTime': serializer.toJson<DateTime?>(adjustmentTime),
|
||||||
|
'latitude': serializer.toJson<double?>(latitude),
|
||||||
|
'longitude': serializer.toJson<double?>(longitude),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -748,6 +897,9 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
i0.Value<String?> checksum = const i0.Value.absent(),
|
i0.Value<String?> checksum = const i0.Value.absent(),
|
||||||
bool? isFavorite,
|
bool? isFavorite,
|
||||||
int? orientation,
|
int? orientation,
|
||||||
|
i0.Value<DateTime?> adjustmentTime = const i0.Value.absent(),
|
||||||
|
i0.Value<double?> latitude = const i0.Value.absent(),
|
||||||
|
i0.Value<double?> longitude = const i0.Value.absent(),
|
||||||
}) => i1.LocalAssetEntityData(
|
}) => i1.LocalAssetEntityData(
|
||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
@@ -762,6 +914,11 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
checksum: checksum.present ? checksum.value : this.checksum,
|
checksum: checksum.present ? checksum.value : this.checksum,
|
||||||
isFavorite: isFavorite ?? this.isFavorite,
|
isFavorite: isFavorite ?? this.isFavorite,
|
||||||
orientation: orientation ?? this.orientation,
|
orientation: orientation ?? this.orientation,
|
||||||
|
adjustmentTime: adjustmentTime.present
|
||||||
|
? adjustmentTime.value
|
||||||
|
: this.adjustmentTime,
|
||||||
|
latitude: latitude.present ? latitude.value : this.latitude,
|
||||||
|
longitude: longitude.present ? longitude.value : this.longitude,
|
||||||
);
|
);
|
||||||
LocalAssetEntityData copyWithCompanion(i1.LocalAssetEntityCompanion data) {
|
LocalAssetEntityData copyWithCompanion(i1.LocalAssetEntityCompanion data) {
|
||||||
return LocalAssetEntityData(
|
return LocalAssetEntityData(
|
||||||
@@ -782,6 +939,11 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
orientation: data.orientation.present
|
orientation: data.orientation.present
|
||||||
? data.orientation.value
|
? data.orientation.value
|
||||||
: this.orientation,
|
: this.orientation,
|
||||||
|
adjustmentTime: data.adjustmentTime.present
|
||||||
|
? data.adjustmentTime.value
|
||||||
|
: this.adjustmentTime,
|
||||||
|
latitude: data.latitude.present ? data.latitude.value : this.latitude,
|
||||||
|
longitude: data.longitude.present ? data.longitude.value : this.longitude,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -798,7 +960,10 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
..write('id: $id, ')
|
..write('id: $id, ')
|
||||||
..write('checksum: $checksum, ')
|
..write('checksum: $checksum, ')
|
||||||
..write('isFavorite: $isFavorite, ')
|
..write('isFavorite: $isFavorite, ')
|
||||||
..write('orientation: $orientation')
|
..write('orientation: $orientation, ')
|
||||||
|
..write('adjustmentTime: $adjustmentTime, ')
|
||||||
|
..write('latitude: $latitude, ')
|
||||||
|
..write('longitude: $longitude')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
@@ -816,6 +981,9 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
checksum,
|
checksum,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
orientation,
|
orientation,
|
||||||
|
adjustmentTime,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
);
|
);
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
@@ -831,7 +999,10 @@ class LocalAssetEntityData extends i0.DataClass
|
|||||||
other.id == this.id &&
|
other.id == this.id &&
|
||||||
other.checksum == this.checksum &&
|
other.checksum == this.checksum &&
|
||||||
other.isFavorite == this.isFavorite &&
|
other.isFavorite == this.isFavorite &&
|
||||||
other.orientation == this.orientation);
|
other.orientation == this.orientation &&
|
||||||
|
other.adjustmentTime == this.adjustmentTime &&
|
||||||
|
other.latitude == this.latitude &&
|
||||||
|
other.longitude == this.longitude);
|
||||||
}
|
}
|
||||||
|
|
||||||
class LocalAssetEntityCompanion
|
class LocalAssetEntityCompanion
|
||||||
@@ -847,6 +1018,9 @@ class LocalAssetEntityCompanion
|
|||||||
final i0.Value<String?> checksum;
|
final i0.Value<String?> checksum;
|
||||||
final i0.Value<bool> isFavorite;
|
final i0.Value<bool> isFavorite;
|
||||||
final i0.Value<int> orientation;
|
final i0.Value<int> orientation;
|
||||||
|
final i0.Value<DateTime?> adjustmentTime;
|
||||||
|
final i0.Value<double?> latitude;
|
||||||
|
final i0.Value<double?> longitude;
|
||||||
const LocalAssetEntityCompanion({
|
const LocalAssetEntityCompanion({
|
||||||
this.name = const i0.Value.absent(),
|
this.name = const i0.Value.absent(),
|
||||||
this.type = const i0.Value.absent(),
|
this.type = const i0.Value.absent(),
|
||||||
@@ -859,6 +1033,9 @@ class LocalAssetEntityCompanion
|
|||||||
this.checksum = const i0.Value.absent(),
|
this.checksum = const i0.Value.absent(),
|
||||||
this.isFavorite = const i0.Value.absent(),
|
this.isFavorite = const i0.Value.absent(),
|
||||||
this.orientation = const i0.Value.absent(),
|
this.orientation = const i0.Value.absent(),
|
||||||
|
this.adjustmentTime = const i0.Value.absent(),
|
||||||
|
this.latitude = const i0.Value.absent(),
|
||||||
|
this.longitude = const i0.Value.absent(),
|
||||||
});
|
});
|
||||||
LocalAssetEntityCompanion.insert({
|
LocalAssetEntityCompanion.insert({
|
||||||
required String name,
|
required String name,
|
||||||
@@ -872,6 +1049,9 @@ class LocalAssetEntityCompanion
|
|||||||
this.checksum = const i0.Value.absent(),
|
this.checksum = const i0.Value.absent(),
|
||||||
this.isFavorite = const i0.Value.absent(),
|
this.isFavorite = const i0.Value.absent(),
|
||||||
this.orientation = const i0.Value.absent(),
|
this.orientation = const i0.Value.absent(),
|
||||||
|
this.adjustmentTime = const i0.Value.absent(),
|
||||||
|
this.latitude = const i0.Value.absent(),
|
||||||
|
this.longitude = const i0.Value.absent(),
|
||||||
}) : name = i0.Value(name),
|
}) : name = i0.Value(name),
|
||||||
type = i0.Value(type),
|
type = i0.Value(type),
|
||||||
id = i0.Value(id);
|
id = i0.Value(id);
|
||||||
@@ -887,6 +1067,9 @@ class LocalAssetEntityCompanion
|
|||||||
i0.Expression<String>? checksum,
|
i0.Expression<String>? checksum,
|
||||||
i0.Expression<bool>? isFavorite,
|
i0.Expression<bool>? isFavorite,
|
||||||
i0.Expression<int>? orientation,
|
i0.Expression<int>? orientation,
|
||||||
|
i0.Expression<DateTime>? adjustmentTime,
|
||||||
|
i0.Expression<double>? latitude,
|
||||||
|
i0.Expression<double>? longitude,
|
||||||
}) {
|
}) {
|
||||||
return i0.RawValuesInsertable({
|
return i0.RawValuesInsertable({
|
||||||
if (name != null) 'name': name,
|
if (name != null) 'name': name,
|
||||||
@@ -900,6 +1083,9 @@ class LocalAssetEntityCompanion
|
|||||||
if (checksum != null) 'checksum': checksum,
|
if (checksum != null) 'checksum': checksum,
|
||||||
if (isFavorite != null) 'is_favorite': isFavorite,
|
if (isFavorite != null) 'is_favorite': isFavorite,
|
||||||
if (orientation != null) 'orientation': orientation,
|
if (orientation != null) 'orientation': orientation,
|
||||||
|
if (adjustmentTime != null) 'adjustment_time': adjustmentTime,
|
||||||
|
if (latitude != null) 'latitude': latitude,
|
||||||
|
if (longitude != null) 'longitude': longitude,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -915,6 +1101,9 @@ class LocalAssetEntityCompanion
|
|||||||
i0.Value<String?>? checksum,
|
i0.Value<String?>? checksum,
|
||||||
i0.Value<bool>? isFavorite,
|
i0.Value<bool>? isFavorite,
|
||||||
i0.Value<int>? orientation,
|
i0.Value<int>? orientation,
|
||||||
|
i0.Value<DateTime?>? adjustmentTime,
|
||||||
|
i0.Value<double?>? latitude,
|
||||||
|
i0.Value<double?>? longitude,
|
||||||
}) {
|
}) {
|
||||||
return i1.LocalAssetEntityCompanion(
|
return i1.LocalAssetEntityCompanion(
|
||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
@@ -928,6 +1117,9 @@ class LocalAssetEntityCompanion
|
|||||||
checksum: checksum ?? this.checksum,
|
checksum: checksum ?? this.checksum,
|
||||||
isFavorite: isFavorite ?? this.isFavorite,
|
isFavorite: isFavorite ?? this.isFavorite,
|
||||||
orientation: orientation ?? this.orientation,
|
orientation: orientation ?? this.orientation,
|
||||||
|
adjustmentTime: adjustmentTime ?? this.adjustmentTime,
|
||||||
|
latitude: latitude ?? this.latitude,
|
||||||
|
longitude: longitude ?? this.longitude,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -969,6 +1161,15 @@ class LocalAssetEntityCompanion
|
|||||||
if (orientation.present) {
|
if (orientation.present) {
|
||||||
map['orientation'] = i0.Variable<int>(orientation.value);
|
map['orientation'] = i0.Variable<int>(orientation.value);
|
||||||
}
|
}
|
||||||
|
if (adjustmentTime.present) {
|
||||||
|
map['adjustment_time'] = i0.Variable<DateTime>(adjustmentTime.value);
|
||||||
|
}
|
||||||
|
if (latitude.present) {
|
||||||
|
map['latitude'] = i0.Variable<double>(latitude.value);
|
||||||
|
}
|
||||||
|
if (longitude.present) {
|
||||||
|
map['longitude'] = i0.Variable<double>(longitude.value);
|
||||||
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -985,7 +1186,10 @@ class LocalAssetEntityCompanion
|
|||||||
..write('id: $id, ')
|
..write('id: $id, ')
|
||||||
..write('checksum: $checksum, ')
|
..write('checksum: $checksum, ')
|
||||||
..write('isFavorite: $isFavorite, ')
|
..write('isFavorite: $isFavorite, ')
|
||||||
..write('orientation: $orientation')
|
..write('orientation: $orientation, ')
|
||||||
|
..write('adjustmentTime: $adjustmentTime, ')
|
||||||
|
..write('latitude: $latitude, ')
|
||||||
|
..write('longitude: $longitude')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
|
|||||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/entities/memory.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/memory.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
|
||||||
@@ -21,6 +20,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.d
|
|||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart';
|
||||||
@@ -95,7 +95,7 @@ class Drift extends $Drift implements IDatabaseRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 13;
|
int get schemaVersion => 14;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
@@ -185,6 +185,11 @@ class Drift extends $Drift implements IDatabaseRepository {
|
|||||||
await m.createIndex(v13.idxTrashedLocalAssetChecksum);
|
await m.createIndex(v13.idxTrashedLocalAssetChecksum);
|
||||||
await m.createIndex(v13.idxTrashedLocalAssetAlbum);
|
await m.createIndex(v13.idxTrashedLocalAssetAlbum);
|
||||||
},
|
},
|
||||||
|
from13To14: (m, v14) async {
|
||||||
|
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.adjustmentTime);
|
||||||
|
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.latitude);
|
||||||
|
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.longitude);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -5485,6 +5485,462 @@ i1.GeneratedColumn<String> _column_95(String aliasedName) =>
|
|||||||
false,
|
false,
|
||||||
type: i1.DriftSqlType.string,
|
type: i1.DriftSqlType.string,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final class Schema14 extends i0.VersionedSchema {
|
||||||
|
Schema14({required super.database}) : super(version: 14);
|
||||||
|
@override
|
||||||
|
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||||
|
userEntity,
|
||||||
|
remoteAssetEntity,
|
||||||
|
stackEntity,
|
||||||
|
localAssetEntity,
|
||||||
|
remoteAlbumEntity,
|
||||||
|
localAlbumEntity,
|
||||||
|
localAlbumAssetEntity,
|
||||||
|
idxLocalAssetChecksum,
|
||||||
|
idxRemoteAssetOwnerChecksum,
|
||||||
|
uQRemoteAssetsOwnerChecksum,
|
||||||
|
uQRemoteAssetsOwnerLibraryChecksum,
|
||||||
|
idxRemoteAssetChecksum,
|
||||||
|
authUserEntity,
|
||||||
|
userMetadataEntity,
|
||||||
|
partnerEntity,
|
||||||
|
remoteExifEntity,
|
||||||
|
remoteAlbumAssetEntity,
|
||||||
|
remoteAlbumUserEntity,
|
||||||
|
memoryEntity,
|
||||||
|
memoryAssetEntity,
|
||||||
|
personEntity,
|
||||||
|
assetFaceEntity,
|
||||||
|
storeEntity,
|
||||||
|
trashedLocalAssetEntity,
|
||||||
|
idxLatLng,
|
||||||
|
idxTrashedLocalAssetChecksum,
|
||||||
|
idxTrashedLocalAssetAlbum,
|
||||||
|
];
|
||||||
|
late final Shape20 userEntity = Shape20(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_3,
|
||||||
|
_column_84,
|
||||||
|
_column_85,
|
||||||
|
_column_91,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape17 remoteAssetEntity = Shape17(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_1,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
_column_12,
|
||||||
|
_column_0,
|
||||||
|
_column_13,
|
||||||
|
_column_14,
|
||||||
|
_column_15,
|
||||||
|
_column_16,
|
||||||
|
_column_17,
|
||||||
|
_column_18,
|
||||||
|
_column_19,
|
||||||
|
_column_20,
|
||||||
|
_column_21,
|
||||||
|
_column_86,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape3 stackEntity = Shape3(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'stack_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [_column_0, _column_9, _column_5, _column_15, _column_75],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape24 localAssetEntity = Shape24(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_1,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
_column_12,
|
||||||
|
_column_0,
|
||||||
|
_column_22,
|
||||||
|
_column_14,
|
||||||
|
_column_23,
|
||||||
|
_column_96,
|
||||||
|
_column_46,
|
||||||
|
_column_47,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape9 remoteAlbumEntity = Shape9(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_56,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_15,
|
||||||
|
_column_57,
|
||||||
|
_column_58,
|
||||||
|
_column_59,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape19 localAlbumEntity = Shape19(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_album_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_5,
|
||||||
|
_column_31,
|
||||||
|
_column_32,
|
||||||
|
_column_90,
|
||||||
|
_column_33,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape22 localAlbumAssetEntity = Shape22(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'local_album_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||||
|
columns: [_column_34, _column_35, _column_33],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
final i1.Index idxLocalAssetChecksum = i1.Index(
|
||||||
|
'idx_local_asset_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
|
||||||
|
'idx_remote_asset_owner_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
|
||||||
|
);
|
||||||
|
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
|
||||||
|
'UQ_remote_assets_owner_checksum',
|
||||||
|
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
|
||||||
|
);
|
||||||
|
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
|
||||||
|
'UQ_remote_assets_owner_library_checksum',
|
||||||
|
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
|
||||||
|
);
|
||||||
|
final i1.Index idxRemoteAssetChecksum = i1.Index(
|
||||||
|
'idx_remote_asset_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
|
||||||
|
);
|
||||||
|
late final Shape21 authUserEntity = Shape21(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'auth_user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_1,
|
||||||
|
_column_3,
|
||||||
|
_column_2,
|
||||||
|
_column_84,
|
||||||
|
_column_85,
|
||||||
|
_column_92,
|
||||||
|
_column_93,
|
||||||
|
_column_7,
|
||||||
|
_column_94,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape4 userMetadataEntity = Shape4(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'user_metadata_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
|
||||||
|
columns: [_column_25, _column_26, _column_27],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape5 partnerEntity = Shape5(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'partner_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
|
||||||
|
columns: [_column_28, _column_29, _column_30],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape8 remoteExifEntity = Shape8(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_exif_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_36,
|
||||||
|
_column_37,
|
||||||
|
_column_38,
|
||||||
|
_column_39,
|
||||||
|
_column_40,
|
||||||
|
_column_41,
|
||||||
|
_column_11,
|
||||||
|
_column_10,
|
||||||
|
_column_42,
|
||||||
|
_column_43,
|
||||||
|
_column_44,
|
||||||
|
_column_45,
|
||||||
|
_column_46,
|
||||||
|
_column_47,
|
||||||
|
_column_48,
|
||||||
|
_column_49,
|
||||||
|
_column_50,
|
||||||
|
_column_51,
|
||||||
|
_column_52,
|
||||||
|
_column_53,
|
||||||
|
_column_54,
|
||||||
|
_column_55,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape7 remoteAlbumAssetEntity = Shape7(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||||
|
columns: [_column_36, _column_60],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape10 remoteAlbumUserEntity = Shape10(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'remote_album_user_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
|
||||||
|
columns: [_column_60, _column_25, _column_61],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape11 memoryEntity = Shape11(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'memory_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_18,
|
||||||
|
_column_15,
|
||||||
|
_column_8,
|
||||||
|
_column_62,
|
||||||
|
_column_63,
|
||||||
|
_column_64,
|
||||||
|
_column_65,
|
||||||
|
_column_66,
|
||||||
|
_column_67,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape12 memoryAssetEntity = Shape12(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'memory_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
|
||||||
|
columns: [_column_36, _column_68],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape14 personEntity = Shape14(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'person_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_15,
|
||||||
|
_column_1,
|
||||||
|
_column_69,
|
||||||
|
_column_71,
|
||||||
|
_column_72,
|
||||||
|
_column_73,
|
||||||
|
_column_74,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape15 assetFaceEntity = Shape15(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'asset_face_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [
|
||||||
|
_column_0,
|
||||||
|
_column_36,
|
||||||
|
_column_76,
|
||||||
|
_column_77,
|
||||||
|
_column_78,
|
||||||
|
_column_79,
|
||||||
|
_column_80,
|
||||||
|
_column_81,
|
||||||
|
_column_82,
|
||||||
|
_column_83,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape18 storeEntity = Shape18(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'store_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id)'],
|
||||||
|
columns: [_column_87, _column_88, _column_89],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
late final Shape23 trashedLocalAssetEntity = Shape23(
|
||||||
|
source: i0.VersionedTable(
|
||||||
|
entityName: 'trashed_local_asset_entity',
|
||||||
|
withoutRowId: true,
|
||||||
|
isStrict: true,
|
||||||
|
tableConstraints: ['PRIMARY KEY(id, album_id)'],
|
||||||
|
columns: [
|
||||||
|
_column_1,
|
||||||
|
_column_8,
|
||||||
|
_column_9,
|
||||||
|
_column_5,
|
||||||
|
_column_10,
|
||||||
|
_column_11,
|
||||||
|
_column_12,
|
||||||
|
_column_0,
|
||||||
|
_column_95,
|
||||||
|
_column_22,
|
||||||
|
_column_14,
|
||||||
|
_column_23,
|
||||||
|
],
|
||||||
|
attachedDatabase: database,
|
||||||
|
),
|
||||||
|
alias: null,
|
||||||
|
);
|
||||||
|
final i1.Index idxLatLng = i1.Index(
|
||||||
|
'idx_lat_lng',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
|
||||||
|
);
|
||||||
|
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
|
||||||
|
'idx_trashed_local_asset_checksum',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
|
||||||
|
);
|
||||||
|
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
|
||||||
|
'idx_trashed_local_asset_album',
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Shape24 extends i0.VersionedTable {
|
||||||
|
Shape24({required super.source, required super.alias}) : super.aliased();
|
||||||
|
i1.GeneratedColumn<String> get name =>
|
||||||
|
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<int> get type =>
|
||||||
|
columnsByName['type']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<int> get width =>
|
||||||
|
columnsByName['width']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get height =>
|
||||||
|
columnsByName['height']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<int> get durationInSeconds =>
|
||||||
|
columnsByName['duration_in_seconds']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<String> get id =>
|
||||||
|
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<String> get checksum =>
|
||||||
|
columnsByName['checksum']! as i1.GeneratedColumn<String>;
|
||||||
|
i1.GeneratedColumn<bool> get isFavorite =>
|
||||||
|
columnsByName['is_favorite']! as i1.GeneratedColumn<bool>;
|
||||||
|
i1.GeneratedColumn<int> get orientation =>
|
||||||
|
columnsByName['orientation']! as i1.GeneratedColumn<int>;
|
||||||
|
i1.GeneratedColumn<DateTime> get adjustmentTime =>
|
||||||
|
columnsByName['adjustment_time']! as i1.GeneratedColumn<DateTime>;
|
||||||
|
i1.GeneratedColumn<double> get latitude =>
|
||||||
|
columnsByName['latitude']! as i1.GeneratedColumn<double>;
|
||||||
|
i1.GeneratedColumn<double> get longitude =>
|
||||||
|
columnsByName['longitude']! as i1.GeneratedColumn<double>;
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.GeneratedColumn<DateTime> _column_96(String aliasedName) =>
|
||||||
|
i1.GeneratedColumn<DateTime>(
|
||||||
|
'adjustment_time',
|
||||||
|
aliasedName,
|
||||||
|
true,
|
||||||
|
type: i1.DriftSqlType.dateTime,
|
||||||
|
);
|
||||||
i0.MigrationStepWithVersion migrationSteps({
|
i0.MigrationStepWithVersion migrationSteps({
|
||||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||||
@@ -5498,6 +5954,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
|
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
|
||||||
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
|
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
|
||||||
required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13,
|
required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
@@ -5561,6 +6018,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from12To13(migrator, schema);
|
await from12To13(migrator, schema);
|
||||||
return 13;
|
return 13;
|
||||||
|
case 13:
|
||||||
|
final schema = Schema14(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from13To14(migrator, schema);
|
||||||
|
return 14;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
}
|
}
|
||||||
@@ -5580,6 +6042,7 @@ i1.OnUpgrade stepByStep({
|
|||||||
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
|
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
|
||||||
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
|
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
|
||||||
required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13,
|
required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
|
||||||
}) => i0.VersionedSchema.stepByStepHelper(
|
}) => i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(
|
step: migrationSteps(
|
||||||
from1To2: from1To2,
|
from1To2: from1To2,
|
||||||
@@ -5594,5 +6057,6 @@ i1.OnUpgrade stepByStep({
|
|||||||
from10To11: from10To11,
|
from10To11: from10To11,
|
||||||
from11To12: from11To12,
|
from11To12: from11To12,
|
||||||
from12To13: from12To13,
|
from12To13: from12To13,
|
||||||
|
from13To14: from13To14,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
@@ -244,7 +246,56 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
|||||||
return query.map((row) => row.readTable(_db.localAssetEntity).toDto()).get();
|
return query.map((row) => row.readTable(_db.localAssetEntity).toDto()).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _upsertAssets(Iterable<LocalAsset> localAssets) {
|
Future<void> Function(Iterable<LocalAsset>) get _upsertAssets =>
|
||||||
|
CurrentPlatform.isIOS ? _upsertAssetsDarwin : _upsertAssetsAndroid;
|
||||||
|
|
||||||
|
Future<void> _upsertAssetsDarwin(Iterable<LocalAsset> localAssets) async {
|
||||||
|
if (localAssets.isEmpty) {
|
||||||
|
return Future.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset checksum if asset changed
|
||||||
|
await _db.batch((batch) async {
|
||||||
|
for (final asset in localAssets) {
|
||||||
|
final companion = LocalAssetEntityCompanion(
|
||||||
|
checksum: const Value(null),
|
||||||
|
adjustmentTime: Value(asset.adjustmentTime),
|
||||||
|
);
|
||||||
|
batch.update(
|
||||||
|
_db.localAssetEntity,
|
||||||
|
companion,
|
||||||
|
where: (row) => row.id.equals(asset.id) & row.adjustmentTime.isNotExp(Variable(asset.adjustmentTime)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return _db.batch((batch) async {
|
||||||
|
for (final asset in localAssets) {
|
||||||
|
final companion = LocalAssetEntityCompanion.insert(
|
||||||
|
name: asset.name,
|
||||||
|
type: asset.type,
|
||||||
|
createdAt: Value(asset.createdAt),
|
||||||
|
updatedAt: Value(asset.updatedAt),
|
||||||
|
width: Value(asset.width),
|
||||||
|
height: Value(asset.height),
|
||||||
|
durationInSeconds: Value(asset.durationInSeconds),
|
||||||
|
id: asset.id,
|
||||||
|
orientation: Value(asset.orientation),
|
||||||
|
isFavorite: Value(asset.isFavorite),
|
||||||
|
latitude: Value(asset.latitude),
|
||||||
|
longitude: Value(asset.longitude),
|
||||||
|
adjustmentTime: Value(asset.adjustmentTime),
|
||||||
|
);
|
||||||
|
batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>(
|
||||||
|
_db.localAssetEntity,
|
||||||
|
companion.copyWith(checksum: const Value(null)),
|
||||||
|
onConflict: DoUpdate((old) => companion),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _upsertAssetsAndroid(Iterable<LocalAsset> localAssets) async {
|
||||||
if (localAssets.isEmpty) {
|
if (localAssets.isEmpty) {
|
||||||
return Future.value();
|
return Future.value();
|
||||||
}
|
}
|
||||||
@@ -260,6 +311,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
|||||||
height: Value(asset.height),
|
height: Value(asset.height),
|
||||||
durationInSeconds: Value(asset.durationInSeconds),
|
durationInSeconds: Value(asset.durationInSeconds),
|
||||||
id: asset.id,
|
id: asset.id,
|
||||||
|
checksum: const Value(null),
|
||||||
orientation: Value(asset.orientation),
|
orientation: Value(asset.orientation),
|
||||||
isFavorite: Value(asset.isFavorite),
|
isFavorite: Value(asset.isFavorite),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ abstract final class ExifDtoConverter {
|
|||||||
f: dto.fNumber?.toDouble(),
|
f: dto.fNumber?.toDouble(),
|
||||||
mm: dto.focalLength?.toDouble(),
|
mm: dto.focalLength?.toDouble(),
|
||||||
iso: dto.iso?.toInt(),
|
iso: dto.iso?.toInt(),
|
||||||
exposureSeconds: _exposureTimeToSeconds(dto.exposureTime),
|
exposureSeconds: exposureTimeToSeconds(dto.exposureTime),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,15 +36,15 @@ abstract final class ExifDtoConverter {
|
|||||||
return isRotated90CW || isRotated270CW;
|
return isRotated90CW || isRotated270CW;
|
||||||
}
|
}
|
||||||
|
|
||||||
static double? _exposureTimeToSeconds(String? s) {
|
static double? exposureTimeToSeconds(String? second) {
|
||||||
if (s == null) {
|
if (second == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
double? value = double.tryParse(s);
|
double? value = double.tryParse(second);
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
final parts = s.split("/");
|
final parts = second.split("/");
|
||||||
if (parts.length == 2) {
|
if (parts.length == 2) {
|
||||||
final numerator = double.tryParse(parts.firstOrNull ?? "-");
|
final numerator = double.tryParse(parts.firstOrNull ?? "-");
|
||||||
final denominator = double.tryParse(parts.lastOrNull ?? "-");
|
final denominator = double.tryParse(parts.lastOrNull ?? "-");
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class MapLocationPickerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
var currentLatLng = LatLng(currentLocation.latitude, currentLocation.longitude);
|
var currentLatLng = LatLng(currentLocation.latitude, currentLocation.longitude);
|
||||||
selectedLatLng.value = currentLatLng;
|
selectedLatLng.value = currentLatLng;
|
||||||
await controller.value?.animateCamera(CameraUpdate.newLatLng(currentLatLng));
|
await controller.value?.animateCamera(CameraUpdate.newLatLngZoom(currentLatLng, 12));
|
||||||
}
|
}
|
||||||
|
|
||||||
return MapThemeOverride(
|
return MapThemeOverride(
|
||||||
@@ -66,7 +66,10 @@ class MapLocationPickerPage extends HookConsumerWidget {
|
|||||||
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(40), bottomRight: Radius.circular(40)),
|
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(40), bottomRight: Radius.circular(40)),
|
||||||
),
|
),
|
||||||
child: MapLibreMap(
|
child: MapLibreMap(
|
||||||
initialCameraPosition: CameraPosition(target: initialLatLng, zoom: 12),
|
initialCameraPosition: CameraPosition(
|
||||||
|
target: initialLatLng,
|
||||||
|
zoom: (initialLatLng.latitude == 0 && initialLatLng.longitude == 0) ? 1 : 12,
|
||||||
|
),
|
||||||
styleString: style,
|
styleString: style,
|
||||||
onMapCreated: (mapController) => controller.value = mapController,
|
onMapCreated: (mapController) => controller.value = mapController,
|
||||||
onStyleLoadedCallback: onStyleLoaded,
|
onStyleLoadedCallback: onStyleLoaded,
|
||||||
|
|||||||
28
mobile/lib/platform/native_sync_api.g.dart
generated
28
mobile/lib/platform/native_sync_api.g.dart
generated
@@ -41,6 +41,9 @@ class PlatformAsset {
|
|||||||
required this.durationInSeconds,
|
required this.durationInSeconds,
|
||||||
required this.orientation,
|
required this.orientation,
|
||||||
required this.isFavorite,
|
required this.isFavorite,
|
||||||
|
this.adjustmentTime,
|
||||||
|
this.latitude,
|
||||||
|
this.longitude,
|
||||||
});
|
});
|
||||||
|
|
||||||
String id;
|
String id;
|
||||||
@@ -63,8 +66,28 @@ class PlatformAsset {
|
|||||||
|
|
||||||
bool isFavorite;
|
bool isFavorite;
|
||||||
|
|
||||||
|
int? adjustmentTime;
|
||||||
|
|
||||||
|
double? latitude;
|
||||||
|
|
||||||
|
double? longitude;
|
||||||
|
|
||||||
List<Object?> _toList() {
|
List<Object?> _toList() {
|
||||||
return <Object?>[id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite];
|
return <Object?>[
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
durationInSeconds,
|
||||||
|
orientation,
|
||||||
|
isFavorite,
|
||||||
|
adjustmentTime,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
Object encode() {
|
Object encode() {
|
||||||
@@ -84,6 +107,9 @@ class PlatformAsset {
|
|||||||
durationInSeconds: result[7]! as int,
|
durationInSeconds: result[7]! as int,
|
||||||
orientation: result[8]! as int,
|
orientation: result[8]! as int,
|
||||||
isFavorite: result[9]! as bool,
|
isFavorite: result[9]! as bool,
|
||||||
|
adjustmentTime: result[10] as int?,
|
||||||
|
latitude: result[11] as double?,
|
||||||
|
longitude: result[12] as double?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
@@ -129,6 +130,15 @@ class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection
|
|||||||
properties.insert(4, _PropertyItem(label: 'Orientation', value: asset.orientation.toString()));
|
properties.insert(4, _PropertyItem(label: 'Orientation', value: asset.orientation.toString()));
|
||||||
final albums = await ref.read(assetServiceProvider).getSourceAlbums(asset.id);
|
final albums = await ref.read(assetServiceProvider).getSourceAlbums(asset.id);
|
||||||
properties.add(_PropertyItem(label: 'Album', value: albums.map((a) => a.name).join(', ')));
|
properties.add(_PropertyItem(label: 'Album', value: albums.map((a) => a.name).join(', ')));
|
||||||
|
if (CurrentPlatform.isIOS) {
|
||||||
|
properties.add(_PropertyItem(label: 'Adjustment Time', value: asset.adjustmentTime?.toString()));
|
||||||
|
}
|
||||||
|
properties.add(
|
||||||
|
_PropertyItem(
|
||||||
|
label: 'GPS Coordinates',
|
||||||
|
value: asset.hasCoordinates ? '${asset.latitude}, ${asset.longitude}' : null,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _addRemoteAssetProperties(RemoteAsset asset) async {
|
Future<void> _addRemoteAssetProperties(RemoteAsset asset) async {
|
||||||
|
|||||||
@@ -251,8 +251,8 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
color: context.textTheme.labelLarge?.color,
|
color: context.textTheme.labelLarge?.color,
|
||||||
),
|
),
|
||||||
subtitle: _getFileInfo(asset, exifInfo),
|
subtitle: _getFileInfo(asset, exifInfo),
|
||||||
subtitleStyle: context.textTheme.bodyMedium?.copyWith(
|
subtitleStyle: context.textTheme.labelMedium?.copyWith(
|
||||||
color: context.textTheme.bodyMedium?.color?.withAlpha(155),
|
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -268,8 +268,8 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
color: context.textTheme.labelLarge?.color,
|
color: context.textTheme.labelLarge?.color,
|
||||||
),
|
),
|
||||||
subtitle: _getFileInfo(asset, exifInfo),
|
subtitle: _getFileInfo(asset, exifInfo),
|
||||||
subtitleStyle: context.textTheme.bodyMedium?.copyWith(
|
subtitleStyle: context.textTheme.labelMedium?.copyWith(
|
||||||
color: context.textTheme.bodyMedium?.color?.withAlpha(155),
|
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -280,7 +280,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
// Asset Date and Time
|
// Asset Date and Time
|
||||||
SheetTile(
|
SheetTile(
|
||||||
title: _getDateTime(context, asset, exifInfo),
|
title: _getDateTime(context, asset, exifInfo),
|
||||||
titleStyle: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
titleStyle: context.textTheme.labelLarge,
|
||||||
trailing: asset.hasRemote && isOwner ? const Icon(Icons.edit, size: 18) : null,
|
trailing: asset.hasRemote && isOwner ? const Icon(Icons.edit, size: 18) : null,
|
||||||
onTap: asset.hasRemote && isOwner ? () async => await _editDateTime(context, ref) : null,
|
onTap: asset.hasRemote && isOwner ? () async => await _editDateTime(context, ref) : null,
|
||||||
),
|
),
|
||||||
@@ -289,7 +289,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
const SheetLocationDetails(),
|
const SheetLocationDetails(),
|
||||||
// Details header
|
// Details header
|
||||||
SheetTile(
|
SheetTile(
|
||||||
title: 'exif_bottom_sheet_details'.t(context: context),
|
title: 'details'.t(context: context).toUpperCase(),
|
||||||
titleStyle: context.textTheme.labelMedium?.copyWith(
|
titleStyle: context.textTheme.labelMedium?.copyWith(
|
||||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@@ -298,29 +298,33 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
// File info
|
// File info
|
||||||
buildFileInfoTile(),
|
buildFileInfoTile(),
|
||||||
// Camera info
|
// Camera info
|
||||||
if (cameraTitle != null)
|
if (cameraTitle != null) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
SheetTile(
|
SheetTile(
|
||||||
title: cameraTitle,
|
title: cameraTitle,
|
||||||
titleStyle: context.textTheme.labelLarge,
|
titleStyle: context.textTheme.labelLarge,
|
||||||
leading: Icon(Icons.camera_alt_outlined, size: 24, color: context.textTheme.labelLarge?.color),
|
leading: Icon(Icons.camera_alt_outlined, size: 24, color: context.textTheme.labelLarge?.color),
|
||||||
subtitle: _getCameraInfoSubtitle(exifInfo),
|
subtitle: _getCameraInfoSubtitle(exifInfo),
|
||||||
subtitleStyle: context.textTheme.bodyMedium?.copyWith(
|
subtitleStyle: context.textTheme.labelMedium?.copyWith(
|
||||||
color: context.textTheme.bodyMedium?.color?.withAlpha(155),
|
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
// Lens info
|
// Lens info
|
||||||
if (lensTitle != null)
|
if (lensTitle != null) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
SheetTile(
|
SheetTile(
|
||||||
title: lensTitle,
|
title: lensTitle,
|
||||||
titleStyle: context.textTheme.labelLarge,
|
titleStyle: context.textTheme.labelLarge,
|
||||||
leading: Icon(Icons.camera_outlined, size: 24, color: context.textTheme.labelLarge?.color),
|
leading: Icon(Icons.camera_outlined, size: 24, color: context.textTheme.labelLarge?.color),
|
||||||
subtitle: _getLensInfoSubtitle(exifInfo),
|
subtitle: _getLensInfoSubtitle(exifInfo),
|
||||||
subtitleStyle: context.textTheme.bodyMedium?.copyWith(
|
subtitleStyle: context.textTheme.labelMedium?.copyWith(
|
||||||
color: context.textTheme.bodyMedium?.color?.withAlpha(155),
|
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
// Appears in (Albums)
|
// Appears in (Albums)
|
||||||
_buildAppearsInList(ref, context),
|
Padding(padding: const EdgeInsets.only(top: 16.0), child: _buildAppearsInList(ref, context)),
|
||||||
// padding at the bottom to avoid cut-off
|
// padding at the bottom to avoid cut-off
|
||||||
const SizedBox(height: 100),
|
const SizedBox(height: 100),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SheetTile(
|
SheetTile(
|
||||||
title: 'exif_bottom_sheet_location'.t(context: context),
|
title: 'location'.t(context: context).toUpperCase(),
|
||||||
titleStyle: context.textTheme.labelMedium?.copyWith(
|
titleStyle: context.textTheme.labelMedium?.copyWith(
|
||||||
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@@ -102,7 +102,7 @@ class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
|
|||||||
Text(
|
Text(
|
||||||
coordinates,
|
coordinates,
|
||||||
style: context.textTheme.labelMedium?.copyWith(
|
style: context.textTheme.labelMedium?.copyWith(
|
||||||
color: context.textTheme.labelMedium?.color?.withAlpha(150),
|
color: context.textTheme.labelMedium?.color?.withAlpha(200),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class SheetTile extends ConsumerWidget {
|
|||||||
} else {
|
} else {
|
||||||
titleWidget = Container(
|
titleWidget = Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.only(left: 15),
|
padding: const EdgeInsets.only(left: 15, right: 15),
|
||||||
child: Text(title, style: titleStyle),
|
child: Text(title, style: titleStyle),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
@@ -150,7 +151,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||||||
try {
|
try {
|
||||||
bool syncSuccess = false;
|
bool syncSuccess = false;
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
_safeRun(backgroundManager.syncLocal(), "syncLocal"),
|
_safeRun(backgroundManager.syncLocal(full: CurrentPlatform.isAndroid ? true : false), "syncLocal"),
|
||||||
_safeRun(backgroundManager.syncRemote().then((success) => syncSuccess = success), "syncRemote"),
|
_safeRun(backgroundManager.syncRemote().then((success) => syncSuccess = success), "syncRemote"),
|
||||||
]);
|
]);
|
||||||
if (syncSuccess) {
|
if (syncSuccess) {
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (version < 19 && Store.isBetaTimelineEnabled) {
|
if (version < 19 && Store.isBetaTimelineEnabled) {
|
||||||
if (!await _populateUpdatedAtTime(drift)) {
|
if (!await _populateLocalAssetTime(drift)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,7 +229,7 @@ Future<void> _migrateDeviceAsset(Isar db) async {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _populateUpdatedAtTime(Drift db) async {
|
Future<bool> _populateLocalAssetTime(Drift db) async {
|
||||||
try {
|
try {
|
||||||
final nativeApi = NativeSyncApi();
|
final nativeApi = NativeSyncApi();
|
||||||
final albums = await nativeApi.getAlbums();
|
final albums = await nativeApi.getAlbums();
|
||||||
@@ -240,6 +240,9 @@ Future<bool> _populateUpdatedAtTime(Drift db) async {
|
|||||||
batch.update(
|
batch.update(
|
||||||
db.localAssetEntity,
|
db.localAssetEntity,
|
||||||
LocalAssetEntityCompanion(
|
LocalAssetEntityCompanion(
|
||||||
|
longitude: Value(asset.longitude),
|
||||||
|
latitude: Value(asset.latitude),
|
||||||
|
adjustmentTime: Value(tryFromSecondsSinceEpoch(asset.adjustmentTime, isUtc: true)),
|
||||||
updatedAt: Value(tryFromSecondsSinceEpoch(asset.updatedAt, isUtc: true) ?? DateTime.timestamp()),
|
updatedAt: Value(tryFromSecondsSinceEpoch(asset.updatedAt, isUtc: true) ?? DateTime.timestamp()),
|
||||||
),
|
),
|
||||||
where: (t) => t.id.equals(asset.id),
|
where: (t) => t.id.equals(asset.id),
|
||||||
@@ -250,7 +253,7 @@ Future<bool> _populateUpdatedAtTime(Drift db) async {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dPrint(() => "[MIGRATION] Error while populating updatedAt time: $error");
|
dPrint(() => "[MIGRATION] Error while populating asset time: $error");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ class PlatformAsset {
|
|||||||
final int orientation;
|
final int orientation;
|
||||||
final bool isFavorite;
|
final bool isFavorite;
|
||||||
|
|
||||||
|
final int? adjustmentTime;
|
||||||
|
final double? latitude;
|
||||||
|
final double? longitude;
|
||||||
|
|
||||||
const PlatformAsset({
|
const PlatformAsset({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
@@ -38,6 +42,9 @@ class PlatformAsset {
|
|||||||
this.durationInSeconds = 0,
|
this.durationInSeconds = 0,
|
||||||
this.orientation = 0,
|
this.orientation = 0,
|
||||||
this.isFavorite = false,
|
this.isFavorite = false,
|
||||||
|
this.adjustmentTime,
|
||||||
|
this.latitude,
|
||||||
|
this.longitude,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
5
mobile/test/drift/main/generated/schema.dart
generated
5
mobile/test/drift/main/generated/schema.dart
generated
@@ -16,6 +16,7 @@ import 'schema_v10.dart' as v10;
|
|||||||
import 'schema_v11.dart' as v11;
|
import 'schema_v11.dart' as v11;
|
||||||
import 'schema_v12.dart' as v12;
|
import 'schema_v12.dart' as v12;
|
||||||
import 'schema_v13.dart' as v13;
|
import 'schema_v13.dart' as v13;
|
||||||
|
import 'schema_v14.dart' as v14;
|
||||||
|
|
||||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||||
@override
|
@override
|
||||||
@@ -47,10 +48,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||||||
return v12.DatabaseAtV12(db);
|
return v12.DatabaseAtV12(db);
|
||||||
case 13:
|
case 13:
|
||||||
return v13.DatabaseAtV13(db);
|
return v13.DatabaseAtV13(db);
|
||||||
|
case 14:
|
||||||
|
return v14.DatabaseAtV14(db);
|
||||||
default:
|
default:
|
||||||
throw MissingSchemaException(version, versions);
|
throw MissingSchemaException(version, versions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
|
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
|
||||||
}
|
}
|
||||||
|
|||||||
7878
mobile/test/drift/main/generated/schema_v14.dart
generated
Normal file
7878
mobile/test/drift/main/generated/schema_v14.dart
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "Monorepo for Immich",
|
"description": "Monorepo for Immich",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@10.22.0+sha512.bf049efe995b28f527fd2b41ae0474ce29186f7edcb3bf545087bd61fbbebb2bf75362d1307fda09c2d288e1e499787ac12d4fcb617a974718a6051f2eee741c",
|
"packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a",
|
||||||
"engines": {
|
"engines": {
|
||||||
"pnpm": ">=10.0.0"
|
"pnpm": ">=10.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
1480
pnpm-lock.yaml
generated
1480
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -45,14 +45,14 @@
|
|||||||
"@nestjs/websockets": "^11.0.4",
|
"@nestjs/websockets": "^11.0.4",
|
||||||
"@opentelemetry/api": "^1.9.0",
|
"@opentelemetry/api": "^1.9.0",
|
||||||
"@opentelemetry/context-async-hooks": "^2.0.0",
|
"@opentelemetry/context-async-hooks": "^2.0.0",
|
||||||
"@opentelemetry/exporter-prometheus": "^0.207.0",
|
"@opentelemetry/exporter-prometheus": "^0.208.0",
|
||||||
"@opentelemetry/instrumentation-http": "^0.207.0",
|
"@opentelemetry/instrumentation-http": "^0.208.0",
|
||||||
"@opentelemetry/instrumentation-ioredis": "^0.55.0",
|
"@opentelemetry/instrumentation-ioredis": "^0.56.0",
|
||||||
"@opentelemetry/instrumentation-nestjs-core": "^0.54.0",
|
"@opentelemetry/instrumentation-nestjs-core": "^0.55.0",
|
||||||
"@opentelemetry/instrumentation-pg": "^0.60.0",
|
"@opentelemetry/instrumentation-pg": "^0.61.0",
|
||||||
"@opentelemetry/resources": "^2.0.1",
|
"@opentelemetry/resources": "^2.0.1",
|
||||||
"@opentelemetry/sdk-metrics": "^2.0.1",
|
"@opentelemetry/sdk-metrics": "^2.0.1",
|
||||||
"@opentelemetry/sdk-node": "^0.207.0",
|
"@opentelemetry/sdk-node": "^0.208.0",
|
||||||
"@opentelemetry/semantic-conventions": "^1.34.0",
|
"@opentelemetry/semantic-conventions": "^1.34.0",
|
||||||
"@react-email/components": "^0.5.0",
|
"@react-email/components": "^0.5.0",
|
||||||
"@react-email/render": "^1.1.2",
|
"@react-email/render": "^1.1.2",
|
||||||
|
|||||||
@@ -369,6 +369,7 @@ select
|
|||||||
"asset"."livePhotoVideoId",
|
"asset"."livePhotoVideoId",
|
||||||
"asset"."encodedVideoPath",
|
"asset"."encodedVideoPath",
|
||||||
"asset"."originalPath",
|
"asset"."originalPath",
|
||||||
|
"asset"."isOffline",
|
||||||
to_json("asset_exif") as "exifInfo",
|
to_json("asset_exif") as "exifInfo",
|
||||||
(
|
(
|
||||||
select
|
select
|
||||||
|
|||||||
@@ -232,6 +232,7 @@ export class AssetJobRepository {
|
|||||||
'asset.livePhotoVideoId',
|
'asset.livePhotoVideoId',
|
||||||
'asset.encodedVideoPath',
|
'asset.encodedVideoPath',
|
||||||
'asset.originalPath',
|
'asset.originalPath',
|
||||||
|
'asset.isOffline',
|
||||||
])
|
])
|
||||||
.$call(withExif)
|
.$call(withExif)
|
||||||
.select(withFacesAndPeople)
|
.select(withFacesAndPeople)
|
||||||
|
|||||||
@@ -24,9 +24,8 @@ export class OAuthRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async authorize(config: OAuthConfig, redirectUrl: string, state?: string, codeChallenge?: string) {
|
async authorize(config: OAuthConfig, redirectUrl: string, state?: string, codeChallenge?: string) {
|
||||||
const { buildAuthorizationUrl, randomState, randomPKCECodeVerifier, calculatePKCECodeChallenge } = await import(
|
const { buildAuthorizationUrl, randomState, randomPKCECodeVerifier, calculatePKCECodeChallenge } =
|
||||||
'openid-client'
|
await import('openid-client');
|
||||||
);
|
|
||||||
const client = await this.getClient(config);
|
const client = await this.getClient(config);
|
||||||
state ??= randomState();
|
state ??= randomState();
|
||||||
|
|
||||||
|
|||||||
@@ -585,8 +585,6 @@ describe(AssetService.name, () => {
|
|||||||
'/uploads/user-id/webp/path.ext',
|
'/uploads/user-id/webp/path.ext',
|
||||||
'/uploads/user-id/thumbs/path.jpg',
|
'/uploads/user-id/thumbs/path.jpg',
|
||||||
'/uploads/user-id/fullsize/path.webp',
|
'/uploads/user-id/fullsize/path.webp',
|
||||||
assetWithFace.encodedVideoPath, // this value is null
|
|
||||||
undefined, // no sidecar path
|
|
||||||
assetWithFace.originalPath,
|
assetWithFace.originalPath,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -648,8 +646,6 @@ describe(AssetService.name, () => {
|
|||||||
'/uploads/user-id/webp/path.ext',
|
'/uploads/user-id/webp/path.ext',
|
||||||
'/uploads/user-id/thumbs/path.jpg',
|
'/uploads/user-id/thumbs/path.jpg',
|
||||||
'/uploads/user-id/fullsize/path.webp',
|
'/uploads/user-id/fullsize/path.webp',
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
'fake_path/asset_1.jpeg',
|
'fake_path/asset_1.jpeg',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -676,8 +672,6 @@ describe(AssetService.name, () => {
|
|||||||
'/uploads/user-id/webp/path.ext',
|
'/uploads/user-id/webp/path.ext',
|
||||||
'/uploads/user-id/thumbs/path.jpg',
|
'/uploads/user-id/thumbs/path.jpg',
|
||||||
'/uploads/user-id/fullsize/path.webp',
|
'/uploads/user-id/fullsize/path.webp',
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
'fake_path/asset_1.jpeg',
|
'fake_path/asset_1.jpeg',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -363,11 +363,11 @@ export class AssetService extends BaseService {
|
|||||||
const { fullsizeFile, previewFile, thumbnailFile, sidecarFile } = getAssetFiles(asset.files ?? []);
|
const { fullsizeFile, previewFile, thumbnailFile, sidecarFile } = getAssetFiles(asset.files ?? []);
|
||||||
const files = [thumbnailFile?.path, previewFile?.path, fullsizeFile?.path, asset.encodedVideoPath];
|
const files = [thumbnailFile?.path, previewFile?.path, fullsizeFile?.path, asset.encodedVideoPath];
|
||||||
|
|
||||||
if (deleteOnDisk) {
|
if (deleteOnDisk && !asset.isOffline) {
|
||||||
files.push(sidecarFile?.path, asset.originalPath);
|
files.push(sidecarFile?.path, asset.originalPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.jobRepository.queue({ name: JobName.FileDelete, data: { files } });
|
await this.jobRepository.queue({ name: JobName.FileDelete, data: { files: files.filter(Boolean) } });
|
||||||
|
|
||||||
return JobStatus.Success;
|
return JobStatus.Success;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,16 @@ import { Kysely } from 'kysely';
|
|||||||
import { AssetFileType, JobName, SharedLinkType } from 'src/enum';
|
import { AssetFileType, JobName, SharedLinkType } from 'src/enum';
|
||||||
import { AccessRepository } from 'src/repositories/access.repository';
|
import { AccessRepository } from 'src/repositories/access.repository';
|
||||||
import { AlbumRepository } from 'src/repositories/album.repository';
|
import { AlbumRepository } from 'src/repositories/album.repository';
|
||||||
|
import { AssetJobRepository } from 'src/repositories/asset-job.repository';
|
||||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||||
|
import { EventRepository } from 'src/repositories/event.repository';
|
||||||
import { JobRepository } from 'src/repositories/job.repository';
|
import { JobRepository } from 'src/repositories/job.repository';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import { SharedLinkAssetRepository } from 'src/repositories/shared-link-asset.repository';
|
import { SharedLinkAssetRepository } from 'src/repositories/shared-link-asset.repository';
|
||||||
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
|
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
|
||||||
import { StackRepository } from 'src/repositories/stack.repository';
|
import { StackRepository } from 'src/repositories/stack.repository';
|
||||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||||
|
import { UserRepository } from 'src/repositories/user.repository';
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
import { AssetService } from 'src/services/asset.service';
|
import { AssetService } from 'src/services/asset.service';
|
||||||
import { newMediumService } from 'test/medium.factory';
|
import { newMediumService } from 'test/medium.factory';
|
||||||
@@ -20,8 +23,16 @@ let defaultDatabase: Kysely<DB>;
|
|||||||
const setup = (db?: Kysely<DB>) => {
|
const setup = (db?: Kysely<DB>) => {
|
||||||
return newMediumService(AssetService, {
|
return newMediumService(AssetService, {
|
||||||
database: db || defaultDatabase,
|
database: db || defaultDatabase,
|
||||||
real: [AssetRepository, AlbumRepository, AccessRepository, SharedLinkAssetRepository, StackRepository],
|
real: [
|
||||||
mock: [LoggingRepository, JobRepository, StorageRepository],
|
AssetRepository,
|
||||||
|
AssetJobRepository,
|
||||||
|
AlbumRepository,
|
||||||
|
AccessRepository,
|
||||||
|
SharedLinkAssetRepository,
|
||||||
|
StackRepository,
|
||||||
|
UserRepository,
|
||||||
|
],
|
||||||
|
mock: [EventRepository, LoggingRepository, JobRepository, StorageRepository],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -210,4 +221,51 @@ describe(AssetService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('delete', () => {
|
||||||
|
it('should delete asset', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
ctx.getMock(EventRepository).emit.mockResolvedValue();
|
||||||
|
ctx.getMock(JobRepository).queue.mockResolvedValue();
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
const thumbnailPath = '/path/to/thumbnail.jpg';
|
||||||
|
const previewPath = '/path/to/preview.jpg';
|
||||||
|
const sidecarPath = '/path/to/sidecar.xmp';
|
||||||
|
await Promise.all([
|
||||||
|
ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Thumbnail, path: thumbnailPath }),
|
||||||
|
ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Preview, path: previewPath }),
|
||||||
|
ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Sidecar, path: sidecarPath }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
|
||||||
|
|
||||||
|
expect(ctx.getMock(JobRepository).queue).toHaveBeenCalledWith({
|
||||||
|
name: JobName.FileDelete,
|
||||||
|
data: { files: [thumbnailPath, previewPath, sidecarPath, asset.originalPath] },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not delete offline assets', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
ctx.getMock(EventRepository).emit.mockResolvedValue();
|
||||||
|
ctx.getMock(JobRepository).queue.mockResolvedValue();
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id, isOffline: true });
|
||||||
|
const thumbnailPath = '/path/to/thumbnail.jpg';
|
||||||
|
const previewPath = '/path/to/preview.jpg';
|
||||||
|
await Promise.all([
|
||||||
|
ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Thumbnail, path: thumbnailPath }),
|
||||||
|
ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Preview, path: previewPath }),
|
||||||
|
ctx.newAssetFile({ assetId: asset.id, type: AssetFileType.Sidecar, path: `/path/to/sidecar.xmp` }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await sut.handleAssetDeletion({ id: asset.id, deleteOnDisk: true });
|
||||||
|
|
||||||
|
expect(ctx.getMock(JobRepository).queue).toHaveBeenCalledWith({
|
||||||
|
name: JobName.FileDelete,
|
||||||
|
data: { files: [thumbnailPath, previewPath] },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -62,7 +62,8 @@
|
|||||||
"svelte-maplibre": "^1.2.5",
|
"svelte-maplibre": "^1.2.5",
|
||||||
"svelte-persisted-store": "^0.12.0",
|
"svelte-persisted-store": "^0.12.0",
|
||||||
"tabbable": "^6.2.0",
|
"tabbable": "^6.2.0",
|
||||||
"thumbhash": "^0.1.1"
|
"thumbhash": "^0.1.1",
|
||||||
|
"uplot": "^1.6.32"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.36.0",
|
"@eslint/js": "^9.36.0",
|
||||||
@@ -70,7 +71,7 @@
|
|||||||
"@koddsson/eslint-plugin-tscompat": "^0.2.0",
|
"@koddsson/eslint-plugin-tscompat": "^0.2.0",
|
||||||
"@socket.io/component-emitter": "^3.1.0",
|
"@socket.io/component-emitter": "^3.1.0",
|
||||||
"@sveltejs/adapter-static": "^3.0.8",
|
"@sveltejs/adapter-static": "^3.0.8",
|
||||||
"@sveltejs/enhanced-img": "^0.8.0",
|
"@sveltejs/enhanced-img": "^0.9.0",
|
||||||
"@sveltejs/kit": "^2.27.1",
|
"@sveltejs/kit": "^2.27.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "6.2.1",
|
"@sveltejs/vite-plugin-svelte": "6.2.1",
|
||||||
"@tailwindcss/vite": "^4.1.7",
|
"@tailwindcss/vite": "^4.1.7",
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import QueueCardBadge from '$lib/components/QueueCardBadge.svelte';
|
||||||
|
import QueueCardButton from '$lib/components/QueueCardButton.svelte';
|
||||||
import Badge from '$lib/elements/Badge.svelte';
|
import Badge from '$lib/elements/Badge.svelte';
|
||||||
|
import { asQueueItem, getQueueDetailUrl } from '$lib/services/queue.service';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { QueueCommand, type QueueCommandDto, type QueueStatisticsDto, type QueueStatusLegacyDto } from '@immich/sdk';
|
import { QueueCommand, type QueueCommandDto, type QueueResponseDto } from '@immich/sdk';
|
||||||
import { Icon, IconButton } from '@immich/ui';
|
import { Icon, IconButton, Link } from '@immich/ui';
|
||||||
import {
|
import {
|
||||||
mdiAlertCircle,
|
mdiAlertCircle,
|
||||||
mdiAllInclusive,
|
mdiAllInclusive,
|
||||||
|
mdiChartLine,
|
||||||
mdiClose,
|
mdiClose,
|
||||||
mdiFastForward,
|
mdiFastForward,
|
||||||
mdiImageRefreshOutline,
|
mdiImageRefreshOutline,
|
||||||
@@ -15,39 +19,23 @@
|
|||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import { type Component } from 'svelte';
|
import { type Component } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import JobTileButton from './JobTileButton.svelte';
|
|
||||||
import JobTileStatus from './JobTileStatus.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
queue: QueueResponseDto;
|
||||||
subtitle: string | undefined;
|
description?: Component;
|
||||||
description: Component | undefined;
|
|
||||||
statistics: QueueStatisticsDto;
|
|
||||||
queueStatus: QueueStatusLegacyDto;
|
|
||||||
icon: string;
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
allText: string | undefined;
|
allText?: string;
|
||||||
refreshText: string | undefined;
|
refreshText?: string;
|
||||||
missingText: string;
|
missingText: string;
|
||||||
onCommand: (command: QueueCommandDto) => void;
|
onCommand: (command: QueueCommandDto) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { queue, description, disabled = false, allText, refreshText, missingText, onCommand }: Props = $props();
|
||||||
title,
|
|
||||||
subtitle,
|
|
||||||
description,
|
|
||||||
statistics,
|
|
||||||
queueStatus,
|
|
||||||
icon,
|
|
||||||
disabled = false,
|
|
||||||
allText,
|
|
||||||
refreshText,
|
|
||||||
missingText,
|
|
||||||
onCommand,
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
|
const { icon, title, subtitle } = $derived(asQueueItem($t, queue));
|
||||||
|
const { statistics } = $derived(queue);
|
||||||
let waitingCount = $derived(statistics.waiting + statistics.paused + statistics.delayed);
|
let waitingCount = $derived(statistics.waiting + statistics.paused + statistics.delayed);
|
||||||
let isIdle = $derived(!queueStatus.isActive && !queueStatus.isPaused);
|
let isIdle = $derived(statistics.active + statistics.waiting === 0 && !queue.isPaused);
|
||||||
let multipleButtons = $derived(allText || refreshText);
|
let multipleButtons = $derived(allText || refreshText);
|
||||||
|
|
||||||
const commonClasses = 'flex place-items-center justify-between w-full py-2 sm:py-4 pe-4 ps-6';
|
const commonClasses = 'flex place-items-center justify-between w-full py-2 sm:py-4 pe-4 ps-6';
|
||||||
@@ -55,17 +43,25 @@
|
|||||||
|
|
||||||
<div class="flex flex-col overflow-hidden rounded-2xl bg-gray-100 dark:bg-immich-dark-gray sm:flex-row sm:rounded-9">
|
<div class="flex flex-col overflow-hidden rounded-2xl bg-gray-100 dark:bg-immich-dark-gray sm:flex-row sm:rounded-9">
|
||||||
<div class="flex w-full flex-col">
|
<div class="flex w-full flex-col">
|
||||||
{#if queueStatus.isPaused}
|
{#if queue.isPaused}
|
||||||
<JobTileStatus color="warning">{$t('paused')}</JobTileStatus>
|
<QueueCardBadge color="warning">{$t('paused')}</QueueCardBadge>
|
||||||
{:else if queueStatus.isActive}
|
{:else if statistics.active > 0}
|
||||||
<JobTileStatus color="success">{$t('active')}</JobTileStatus>
|
<QueueCardBadge color="success">{$t('active')}</QueueCardBadge>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex flex-col gap-2 p-5 sm:p-7 md:p-9">
|
<div class="flex flex-col gap-2 p-5 sm:p-7 md:p-9">
|
||||||
<div class="flex items-center gap-4 text-xl font-semibold text-primary">
|
<div class="flex items-center gap-2 text-xl font-semibold text-primary">
|
||||||
<span class="flex items-center gap-2">
|
<Link class="flex items-center gap-2 hover:underline" href={getQueueDetailUrl(queue)} underline={false}>
|
||||||
<Icon {icon} size="1.25em" class="hidden shrink-0 sm:block" />
|
<Icon {icon} size="1.25em" class="hidden shrink-0 sm:block" />
|
||||||
<span class="uppercase">{title}</span>
|
<span class="uppercase">{title}</span>
|
||||||
</span>
|
</Link>
|
||||||
|
<IconButton
|
||||||
|
color="primary"
|
||||||
|
icon={mdiChartLine}
|
||||||
|
aria-label={$t('view_details')}
|
||||||
|
size="small"
|
||||||
|
variant="ghost"
|
||||||
|
href={getQueueDetailUrl(queue)}
|
||||||
|
/>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
{#if statistics.failed > 0}
|
{#if statistics.failed > 0}
|
||||||
<Badge>
|
<Badge>
|
||||||
@@ -128,62 +124,62 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex w-full flex-row overflow-hidden sm:w-32 sm:flex-col">
|
<div class="flex w-full flex-row overflow-hidden sm:w-32 sm:flex-col">
|
||||||
{#if disabled}
|
{#if disabled}
|
||||||
<JobTileButton
|
<QueueCardButton
|
||||||
disabled={true}
|
disabled={true}
|
||||||
color="light-gray"
|
color="light-gray"
|
||||||
onClick={() => onCommand({ command: QueueCommand.Start, force: false })}
|
onClick={() => onCommand({ command: QueueCommand.Start, force: false })}
|
||||||
>
|
>
|
||||||
<Icon icon={mdiAlertCircle} size="36" />
|
<Icon icon={mdiAlertCircle} size="36" />
|
||||||
<span class="uppercase">{$t('disabled')}</span>
|
<span class="uppercase">{$t('disabled')}</span>
|
||||||
</JobTileButton>
|
</QueueCardButton>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !disabled && !isIdle}
|
{#if !disabled && !isIdle}
|
||||||
{#if waitingCount > 0}
|
{#if waitingCount > 0}
|
||||||
<JobTileButton color="gray" onClick={() => onCommand({ command: QueueCommand.Empty, force: false })}>
|
<QueueCardButton color="gray" onClick={() => onCommand({ command: QueueCommand.Empty, force: false })}>
|
||||||
<Icon icon={mdiClose} size="24" />
|
<Icon icon={mdiClose} size="24" />
|
||||||
<span class="uppercase">{$t('clear')}</span>
|
<span class="uppercase">{$t('clear')}</span>
|
||||||
</JobTileButton>
|
</QueueCardButton>
|
||||||
{/if}
|
{/if}
|
||||||
{#if queueStatus.isPaused}
|
{#if queue.isPaused}
|
||||||
{@const size = waitingCount > 0 ? '24' : '48'}
|
{@const size = waitingCount > 0 ? '24' : '48'}
|
||||||
<JobTileButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Resume, force: false })}>
|
<QueueCardButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Resume, force: false })}>
|
||||||
<!-- size property is not reactive, so have to use width and height -->
|
<!-- size property is not reactive, so have to use width and height -->
|
||||||
<Icon icon={mdiFastForward} {size} />
|
<Icon icon={mdiFastForward} {size} />
|
||||||
<span class="uppercase">{$t('resume')}</span>
|
<span class="uppercase">{$t('resume')}</span>
|
||||||
</JobTileButton>
|
</QueueCardButton>
|
||||||
{:else}
|
{:else}
|
||||||
<JobTileButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Pause, force: false })}>
|
<QueueCardButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Pause, force: false })}>
|
||||||
<Icon icon={mdiPause} size="24" />
|
<Icon icon={mdiPause} size="24" />
|
||||||
<span class="uppercase">{$t('pause')}</span>
|
<span class="uppercase">{$t('pause')}</span>
|
||||||
</JobTileButton>
|
</QueueCardButton>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !disabled && multipleButtons && isIdle}
|
{#if !disabled && multipleButtons && isIdle}
|
||||||
{#if allText}
|
{#if allText}
|
||||||
<JobTileButton color="dark-gray" onClick={() => onCommand({ command: QueueCommand.Start, force: true })}>
|
<QueueCardButton color="dark-gray" onClick={() => onCommand({ command: QueueCommand.Start, force: true })}>
|
||||||
<Icon icon={mdiAllInclusive} size="24" />
|
<Icon icon={mdiAllInclusive} size="24" />
|
||||||
<span class="uppercase">{allText}</span>
|
<span class="uppercase">{allText}</span>
|
||||||
</JobTileButton>
|
</QueueCardButton>
|
||||||
{/if}
|
{/if}
|
||||||
{#if refreshText}
|
{#if refreshText}
|
||||||
<JobTileButton color="gray" onClick={() => onCommand({ command: QueueCommand.Start, force: undefined })}>
|
<QueueCardButton color="gray" onClick={() => onCommand({ command: QueueCommand.Start, force: undefined })}>
|
||||||
<Icon icon={mdiImageRefreshOutline} size="24" />
|
<Icon icon={mdiImageRefreshOutline} size="24" />
|
||||||
<span class="uppercase">{refreshText}</span>
|
<span class="uppercase">{refreshText}</span>
|
||||||
</JobTileButton>
|
</QueueCardButton>
|
||||||
{/if}
|
{/if}
|
||||||
<JobTileButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Start, force: false })}>
|
<QueueCardButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Start, force: false })}>
|
||||||
<Icon icon={mdiSelectionSearch} size="24" />
|
<Icon icon={mdiSelectionSearch} size="24" />
|
||||||
<span class="uppercase">{missingText}</span>
|
<span class="uppercase">{missingText}</span>
|
||||||
</JobTileButton>
|
</QueueCardButton>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !disabled && !multipleButtons && isIdle}
|
{#if !disabled && !multipleButtons && isIdle}
|
||||||
<JobTileButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Start, force: false })}>
|
<QueueCardButton color="light-gray" onClick={() => onCommand({ command: QueueCommand.Start, force: false })}>
|
||||||
<Icon icon={mdiPlay} size="48" />
|
<Icon icon={mdiPlay} size="48" />
|
||||||
<span class="uppercase">{missingText}</span>
|
<span class="uppercase">{missingText}</span>
|
||||||
</JobTileButton>
|
</QueueCardButton>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
160
web/src/lib/components/QueueGraph.svelte
Normal file
160
web/src/lib/components/QueueGraph.svelte
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { queueManager } from '$lib/managers/queue-manager.svelte';
|
||||||
|
import type { QueueSnapshot } from '$lib/types';
|
||||||
|
import type { QueueResponseDto } from '@immich/sdk';
|
||||||
|
import { LoadingSpinner, Theme, theme } from '@immich/ui';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import uPlot, { type AlignedData, type Axis } from 'uplot';
|
||||||
|
import 'uplot/dist/uPlot.min.css';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
queue: QueueResponseDto;
|
||||||
|
class?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { queue, class: className = '' }: Props = $props();
|
||||||
|
|
||||||
|
type Data = number | null;
|
||||||
|
type NormalizedData = [
|
||||||
|
Data[], // timestamps
|
||||||
|
Data[], // failed counts
|
||||||
|
Data[], // active counts
|
||||||
|
Data[], // waiting counts
|
||||||
|
];
|
||||||
|
|
||||||
|
const normalizeData = (snapshots: QueueSnapshot[]) => {
|
||||||
|
const items: NormalizedData = [[], [], [], []];
|
||||||
|
|
||||||
|
for (const { timestamp, snapshot } of snapshots) {
|
||||||
|
items[0].push(timestamp);
|
||||||
|
|
||||||
|
const statistics = (snapshot || []).find(({ name }) => name === queue.name)?.statistics;
|
||||||
|
|
||||||
|
if (statistics) {
|
||||||
|
items[1].push(statistics.failed);
|
||||||
|
items[2].push(statistics.active);
|
||||||
|
items[3].push(statistics.waiting + statistics.paused);
|
||||||
|
} else {
|
||||||
|
items[0].push(timestamp);
|
||||||
|
items[1].push(null);
|
||||||
|
items[2].push(null);
|
||||||
|
items[3].push(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items[0].push(Date.now() + 5000);
|
||||||
|
items[1].push(items[1].at(-1) ?? 0);
|
||||||
|
items[2].push(items[2].at(-1) ?? 0);
|
||||||
|
items[3].push(items[3].at(-1) ?? 0);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = $derived(normalizeData(queueManager.snapshots));
|
||||||
|
|
||||||
|
let chartElement: HTMLDivElement | undefined = $state();
|
||||||
|
let isDark = $derived(theme.value === Theme.Dark);
|
||||||
|
let plot: uPlot;
|
||||||
|
|
||||||
|
const axisOptions: Axis = {
|
||||||
|
stroke: () => (isDark ? '#ccc' : 'black'),
|
||||||
|
ticks: {
|
||||||
|
show: true,
|
||||||
|
stroke: () => (isDark ? '#444' : '#ddd'),
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
show: true,
|
||||||
|
stroke: () => (isDark ? '#444' : '#ddd'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const seriesOptions: uPlot.Series = {
|
||||||
|
spanGaps: false,
|
||||||
|
points: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
width: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const options: uPlot.Options = {
|
||||||
|
legend: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
cursor: {
|
||||||
|
show: false,
|
||||||
|
lock: true,
|
||||||
|
drag: {
|
||||||
|
setScale: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
width: 200,
|
||||||
|
height: 200,
|
||||||
|
ms: 1,
|
||||||
|
pxAlign: true,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
distr: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
stroke: '#d94a4a',
|
||||||
|
...seriesOptions,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stroke: '#4250af',
|
||||||
|
...seriesOptions,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stroke: '#1075db',
|
||||||
|
...seriesOptions,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
axes: [
|
||||||
|
{
|
||||||
|
...axisOptions,
|
||||||
|
values: (plot, values) => {
|
||||||
|
return values.map((value) => {
|
||||||
|
if (!value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return DateTime.fromMillis(value).toFormat('hh:mm:ss');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
axisOptions,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const onThemeChange = () => plot?.redraw(false);
|
||||||
|
|
||||||
|
$effect(() => theme.value && onThemeChange());
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
plot = new uPlot(options, data as AlignedData, chartElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
const update = () => {
|
||||||
|
if (plot && chartElement && data[0].length > 0) {
|
||||||
|
const now = Date.now();
|
||||||
|
const scale = { min: now - chartElement!.clientWidth * 100, max: now };
|
||||||
|
|
||||||
|
plot.setData(data as AlignedData, false);
|
||||||
|
plot.setScale('x', scale);
|
||||||
|
plot.setSize({ width: chartElement.clientWidth, height: chartElement.clientHeight });
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(update);
|
||||||
|
};
|
||||||
|
|
||||||
|
requestAnimationFrame(update);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full {className}" bind:this={chartElement}>
|
||||||
|
{#if data[0].length === 0}
|
||||||
|
<LoadingSpinner size="giant" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
132
web/src/lib/components/QueuePanel.svelte
Normal file
132
web/src/lib/components/QueuePanel.svelte
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import QueueCard from '$lib/components/QueueCard.svelte';
|
||||||
|
import QueueStorageMigrationDescription from '$lib/components/QueueStorageMigrationDescription.svelte';
|
||||||
|
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||||
|
import { queueManager } from '$lib/managers/queue-manager.svelte';
|
||||||
|
import { asQueueItem } from '$lib/services/queue.service';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import {
|
||||||
|
QueueCommand,
|
||||||
|
type QueueCommandDto,
|
||||||
|
QueueName,
|
||||||
|
type QueueResponseDto,
|
||||||
|
runQueueCommandLegacy,
|
||||||
|
} from '@immich/sdk';
|
||||||
|
import { modalManager, toastManager } from '@immich/ui';
|
||||||
|
import type { Component } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
queues: QueueResponseDto[];
|
||||||
|
};
|
||||||
|
|
||||||
|
let { queues }: Props = $props();
|
||||||
|
const featureFlags = featureFlagsManager.value;
|
||||||
|
|
||||||
|
type QueueDetails = {
|
||||||
|
description?: Component;
|
||||||
|
allText?: string;
|
||||||
|
refreshText?: string;
|
||||||
|
missingText: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
handleCommand?: (jobId: QueueName, jobCommand: QueueCommandDto) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const queueDetails: Partial<Record<QueueName, QueueDetails>> = {
|
||||||
|
[QueueName.ThumbnailGeneration]: {
|
||||||
|
allText: $t('all'),
|
||||||
|
missingText: $t('missing'),
|
||||||
|
},
|
||||||
|
[QueueName.MetadataExtraction]: {
|
||||||
|
allText: $t('all'),
|
||||||
|
missingText: $t('missing'),
|
||||||
|
},
|
||||||
|
[QueueName.Library]: {
|
||||||
|
missingText: $t('rescan'),
|
||||||
|
},
|
||||||
|
[QueueName.Sidecar]: {
|
||||||
|
allText: $t('sync'),
|
||||||
|
missingText: $t('discover'),
|
||||||
|
disabled: !featureFlags.sidecar,
|
||||||
|
},
|
||||||
|
[QueueName.SmartSearch]: {
|
||||||
|
allText: $t('all'),
|
||||||
|
missingText: $t('missing'),
|
||||||
|
disabled: !featureFlags.smartSearch,
|
||||||
|
},
|
||||||
|
[QueueName.DuplicateDetection]: {
|
||||||
|
allText: $t('all'),
|
||||||
|
missingText: $t('missing'),
|
||||||
|
disabled: !featureFlags.duplicateDetection,
|
||||||
|
},
|
||||||
|
[QueueName.FaceDetection]: {
|
||||||
|
allText: $t('reset'),
|
||||||
|
refreshText: $t('refresh'),
|
||||||
|
missingText: $t('missing'),
|
||||||
|
disabled: !featureFlags.facialRecognition,
|
||||||
|
},
|
||||||
|
[QueueName.FacialRecognition]: {
|
||||||
|
allText: $t('reset'),
|
||||||
|
missingText: $t('missing'),
|
||||||
|
disabled: !featureFlags.facialRecognition,
|
||||||
|
},
|
||||||
|
[QueueName.Ocr]: {
|
||||||
|
allText: $t('all'),
|
||||||
|
missingText: $t('missing'),
|
||||||
|
disabled: !featureFlags.ocr,
|
||||||
|
},
|
||||||
|
[QueueName.VideoConversion]: {
|
||||||
|
allText: $t('all'),
|
||||||
|
missingText: $t('missing'),
|
||||||
|
},
|
||||||
|
[QueueName.StorageTemplateMigration]: {
|
||||||
|
missingText: $t('start'),
|
||||||
|
description: QueueStorageMigrationDescription,
|
||||||
|
},
|
||||||
|
[QueueName.Migration]: {
|
||||||
|
missingText: $t('start'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let queueList = Object.entries(queueDetails) as [QueueName, QueueDetails][];
|
||||||
|
|
||||||
|
const handleCommand = async (name: QueueName, dto: QueueCommandDto) => {
|
||||||
|
const item = asQueueItem($t, { name });
|
||||||
|
|
||||||
|
switch (name) {
|
||||||
|
case QueueName.FaceDetection:
|
||||||
|
case QueueName.FacialRecognition: {
|
||||||
|
if (dto.force) {
|
||||||
|
const confirmed = await modalManager.showDialog({ prompt: $t('admin.confirm_reprocess_all_faces') });
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await runQueueCommandLegacy({ name, queueCommandDto: dto });
|
||||||
|
await queueManager.refresh();
|
||||||
|
|
||||||
|
switch (dto.command) {
|
||||||
|
case QueueCommand.Empty: {
|
||||||
|
toastManager.success($t('admin.cleared_jobs', { values: { job: item.title } }));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('admin.failed_job_command', { values: { command: dto.command, job: item.title } }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-7 mt-10">
|
||||||
|
{#each queueList as [queueName, props] (queueName)}
|
||||||
|
{@const queue = queues.find(({ name }) => name === queueName)}
|
||||||
|
{#if queue}
|
||||||
|
<QueueCard {queue} onCommand={(command) => handleCommand(queueName, command)} {...props} />
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
@@ -254,7 +254,7 @@
|
|||||||
values={{ job: $t('admin.storage_template_migration_job') }}
|
values={{ job: $t('admin.storage_template_migration_job') }}
|
||||||
>
|
>
|
||||||
{#snippet children({ message })}
|
{#snippet children({ message })}
|
||||||
<a href={resolve(AppRoute.ADMIN_JOBS)} class="text-primary">
|
<a href={resolve(AppRoute.ADMIN_QUEUES)} class="text-primary">
|
||||||
{message}
|
{message}
|
||||||
</a>
|
</a>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@
|
|||||||
<ControlAppBar showBackButton={false}>
|
<ControlAppBar showBackButton={false}>
|
||||||
{#snippet leading()}
|
{#snippet leading()}
|
||||||
<a data-sveltekit-preload-data="hover" class="ms-4" href="/">
|
<a data-sveltekit-preload-data="hover" class="ms-4" href="/">
|
||||||
<Logo variant="inline" />
|
<Logo variant="inline" class="min-w-min" />
|
||||||
</a>
|
</a>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
|
|||||||
@@ -1,197 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
|
||||||
import { getQueueName } from '$lib/utils';
|
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
|
||||||
import {
|
|
||||||
QueueCommand,
|
|
||||||
type QueueCommandDto,
|
|
||||||
QueueName,
|
|
||||||
type QueuesResponseLegacyDto,
|
|
||||||
runQueueCommandLegacy,
|
|
||||||
} from '@immich/sdk';
|
|
||||||
import { modalManager, toastManager } from '@immich/ui';
|
|
||||||
import {
|
|
||||||
mdiContentDuplicate,
|
|
||||||
mdiFaceRecognition,
|
|
||||||
mdiFileJpgBox,
|
|
||||||
mdiFileXmlBox,
|
|
||||||
mdiFolderMove,
|
|
||||||
mdiImageSearch,
|
|
||||||
mdiLibraryShelves,
|
|
||||||
mdiOcr,
|
|
||||||
mdiTable,
|
|
||||||
mdiTagFaces,
|
|
||||||
mdiVideo,
|
|
||||||
} from '@mdi/js';
|
|
||||||
import type { Component } from 'svelte';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
import JobTile from './JobTile.svelte';
|
|
||||||
import StorageMigrationDescription from './StorageMigrationDescription.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
jobs: QueuesResponseLegacyDto;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { jobs = $bindable() }: Props = $props();
|
|
||||||
const featureFlags = featureFlagsManager.value;
|
|
||||||
|
|
||||||
type JobDetails = {
|
|
||||||
title: string;
|
|
||||||
subtitle?: string;
|
|
||||||
description?: Component;
|
|
||||||
allText?: string;
|
|
||||||
refreshText?: string;
|
|
||||||
missingText: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
icon: string;
|
|
||||||
handleCommand?: (jobId: QueueName, jobCommand: QueueCommandDto) => Promise<void>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConfirmCommand = async (jobId: QueueName, dto: QueueCommandDto) => {
|
|
||||||
if (dto.force) {
|
|
||||||
const isConfirmed = await modalManager.showDialog({
|
|
||||||
prompt: $t('admin.confirm_reprocess_all_faces'),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isConfirmed) {
|
|
||||||
await handleCommand(jobId, { command: QueueCommand.Start, force: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await handleCommand(jobId, dto);
|
|
||||||
};
|
|
||||||
|
|
||||||
let jobDetails: Partial<Record<QueueName, JobDetails>> = {
|
|
||||||
[QueueName.ThumbnailGeneration]: {
|
|
||||||
icon: mdiFileJpgBox,
|
|
||||||
title: $getQueueName(QueueName.ThumbnailGeneration),
|
|
||||||
subtitle: $t('admin.thumbnail_generation_job_description'),
|
|
||||||
allText: $t('all'),
|
|
||||||
missingText: $t('missing'),
|
|
||||||
},
|
|
||||||
[QueueName.MetadataExtraction]: {
|
|
||||||
icon: mdiTable,
|
|
||||||
title: $getQueueName(QueueName.MetadataExtraction),
|
|
||||||
subtitle: $t('admin.metadata_extraction_job_description'),
|
|
||||||
allText: $t('all'),
|
|
||||||
missingText: $t('missing'),
|
|
||||||
},
|
|
||||||
[QueueName.Library]: {
|
|
||||||
icon: mdiLibraryShelves,
|
|
||||||
title: $getQueueName(QueueName.Library),
|
|
||||||
subtitle: $t('admin.library_tasks_description'),
|
|
||||||
missingText: $t('rescan'),
|
|
||||||
},
|
|
||||||
[QueueName.Sidecar]: {
|
|
||||||
title: $getQueueName(QueueName.Sidecar),
|
|
||||||
icon: mdiFileXmlBox,
|
|
||||||
subtitle: $t('admin.sidecar_job_description'),
|
|
||||||
allText: $t('sync'),
|
|
||||||
missingText: $t('discover'),
|
|
||||||
disabled: !featureFlags.sidecar,
|
|
||||||
},
|
|
||||||
[QueueName.SmartSearch]: {
|
|
||||||
icon: mdiImageSearch,
|
|
||||||
title: $getQueueName(QueueName.SmartSearch),
|
|
||||||
subtitle: $t('admin.smart_search_job_description'),
|
|
||||||
allText: $t('all'),
|
|
||||||
missingText: $t('missing'),
|
|
||||||
disabled: !featureFlags.smartSearch,
|
|
||||||
},
|
|
||||||
[QueueName.DuplicateDetection]: {
|
|
||||||
icon: mdiContentDuplicate,
|
|
||||||
title: $getQueueName(QueueName.DuplicateDetection),
|
|
||||||
subtitle: $t('admin.duplicate_detection_job_description'),
|
|
||||||
allText: $t('all'),
|
|
||||||
missingText: $t('missing'),
|
|
||||||
disabled: !featureFlags.duplicateDetection,
|
|
||||||
},
|
|
||||||
[QueueName.FaceDetection]: {
|
|
||||||
icon: mdiFaceRecognition,
|
|
||||||
title: $getQueueName(QueueName.FaceDetection),
|
|
||||||
subtitle: $t('admin.face_detection_description'),
|
|
||||||
allText: $t('reset'),
|
|
||||||
refreshText: $t('refresh'),
|
|
||||||
missingText: $t('missing'),
|
|
||||||
handleCommand: handleConfirmCommand,
|
|
||||||
disabled: !featureFlags.facialRecognition,
|
|
||||||
},
|
|
||||||
[QueueName.FacialRecognition]: {
|
|
||||||
icon: mdiTagFaces,
|
|
||||||
title: $getQueueName(QueueName.FacialRecognition),
|
|
||||||
subtitle: $t('admin.facial_recognition_job_description'),
|
|
||||||
allText: $t('reset'),
|
|
||||||
missingText: $t('missing'),
|
|
||||||
handleCommand: handleConfirmCommand,
|
|
||||||
disabled: !featureFlags.facialRecognition,
|
|
||||||
},
|
|
||||||
[QueueName.Ocr]: {
|
|
||||||
icon: mdiOcr,
|
|
||||||
title: $getQueueName(QueueName.Ocr),
|
|
||||||
subtitle: $t('admin.ocr_job_description'),
|
|
||||||
allText: $t('all'),
|
|
||||||
missingText: $t('missing'),
|
|
||||||
disabled: !featureFlags.ocr,
|
|
||||||
},
|
|
||||||
[QueueName.VideoConversion]: {
|
|
||||||
icon: mdiVideo,
|
|
||||||
title: $getQueueName(QueueName.VideoConversion),
|
|
||||||
subtitle: $t('admin.video_conversion_job_description'),
|
|
||||||
allText: $t('all'),
|
|
||||||
missingText: $t('missing'),
|
|
||||||
},
|
|
||||||
[QueueName.StorageTemplateMigration]: {
|
|
||||||
icon: mdiFolderMove,
|
|
||||||
title: $getQueueName(QueueName.StorageTemplateMigration),
|
|
||||||
missingText: $t('start'),
|
|
||||||
description: StorageMigrationDescription,
|
|
||||||
},
|
|
||||||
[QueueName.Migration]: {
|
|
||||||
icon: mdiFolderMove,
|
|
||||||
title: $getQueueName(QueueName.Migration),
|
|
||||||
subtitle: $t('admin.migration_job_description'),
|
|
||||||
missingText: $t('start'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let jobList = Object.entries(jobDetails) as [QueueName, JobDetails][];
|
|
||||||
|
|
||||||
async function handleCommand(name: QueueName, dto: QueueCommandDto) {
|
|
||||||
const title = jobDetails[name]?.title;
|
|
||||||
|
|
||||||
try {
|
|
||||||
jobs[name] = await runQueueCommandLegacy({ name, queueCommandDto: dto });
|
|
||||||
|
|
||||||
switch (dto.command) {
|
|
||||||
case QueueCommand.Empty: {
|
|
||||||
toastManager.success($t('admin.cleared_jobs', { values: { job: title } }));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('admin.failed_job_command', { values: { command: dto.command, job: title } }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-7">
|
|
||||||
{#each jobList as [jobName, { title, subtitle, description, disabled, allText, refreshText, missingText, icon, handleCommand: handleCommandOverride }] (jobName)}
|
|
||||||
{@const { jobCounts: statistics, queueStatus } = jobs[jobName]}
|
|
||||||
<JobTile
|
|
||||||
{icon}
|
|
||||||
{title}
|
|
||||||
{disabled}
|
|
||||||
{subtitle}
|
|
||||||
{description}
|
|
||||||
{allText}
|
|
||||||
{refreshText}
|
|
||||||
{missingText}
|
|
||||||
{statistics}
|
|
||||||
{queueStatus}
|
|
||||||
onCommand={(command) => (handleCommandOverride || handleCommand)(jobName, command)}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
@@ -376,7 +376,7 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#snippet children({ feature }: { feature: Feature<Geometry, GeoJsonProperties> })}
|
{#snippet children({ feature }: { feature: Feature })}
|
||||||
{#if useLocationPin}
|
{#if useLocationPin}
|
||||||
<Icon icon={mdiMapMarker} size="50px" class="text-primary -translate-y-[50%]" />
|
<Icon icon={mdiMapMarker} size="50px" class="text-primary -translate-y-[50%]" />
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
import { generateId } from '$lib/utils/generate-id';
|
import { generateId } from '$lib/utils/generate-id';
|
||||||
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
|
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
|
||||||
import type { MetadataSearchDto, SmartSearchDto } from '@immich/sdk';
|
import type { MetadataSearchDto, SmartSearchDto } from '@immich/sdk';
|
||||||
import { IconButton, modalManager } from '@immich/ui';
|
import { Button, IconButton, modalManager } from '@immich/ui';
|
||||||
import { mdiClose, mdiMagnify, mdiTune } from '@mdi/js';
|
import { mdiClose, mdiMagnify, mdiTune } from '@mdi/js';
|
||||||
import { onDestroy, tick } from 'svelte';
|
import { onDestroy, onMount, tick } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import SearchHistoryBox from './search-history-box.svelte';
|
import SearchHistoryBox from './search-history-box.svelte';
|
||||||
|
|
||||||
@@ -31,6 +31,8 @@
|
|||||||
let isSearchSuggestions = $state(false);
|
let isSearchSuggestions = $state(false);
|
||||||
let selectedId: string | undefined = $state();
|
let selectedId: string | undefined = $state();
|
||||||
let close: (() => Promise<void>) | undefined;
|
let close: (() => Promise<void>) | undefined;
|
||||||
|
let showSearchTypeDropdown = $state(false);
|
||||||
|
let currentSearchType = $state('smart');
|
||||||
|
|
||||||
const listboxId = generateId();
|
const listboxId = generateId();
|
||||||
const searchTypeId = generateId();
|
const searchTypeId = generateId();
|
||||||
@@ -70,6 +72,7 @@
|
|||||||
|
|
||||||
const onFocusIn = () => {
|
const onFocusIn = () => {
|
||||||
searchStore.isSearchEnabled = true;
|
searchStore.isSearchEnabled = true;
|
||||||
|
getSearchType();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFocusOut = () => {
|
const onFocusOut = () => {
|
||||||
@@ -98,6 +101,9 @@
|
|||||||
const searchResult = await result.onClose;
|
const searchResult = await result.onClose;
|
||||||
close = undefined;
|
close = undefined;
|
||||||
|
|
||||||
|
// Refresh search type after modal closes
|
||||||
|
getSearchType();
|
||||||
|
|
||||||
if (!searchResult) {
|
if (!searchResult) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -139,6 +145,7 @@
|
|||||||
|
|
||||||
const onEscape = () => {
|
const onEscape = () => {
|
||||||
closeDropdown();
|
closeDropdown();
|
||||||
|
closeSearchTypeDropdown();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onArrow = async (direction: 1 | -1) => {
|
const onArrow = async (direction: 1 | -1) => {
|
||||||
@@ -168,6 +175,20 @@
|
|||||||
searchHistoryBox?.clearSelection();
|
searchHistoryBox?.clearSelection();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleSearchTypeDropdown = () => {
|
||||||
|
showSearchTypeDropdown = !showSearchTypeDropdown;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeSearchTypeDropdown = () => {
|
||||||
|
showSearchTypeDropdown = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectSearchType = (type: string) => {
|
||||||
|
localStorage.setItem('searchQueryType', type);
|
||||||
|
currentSearchType = type;
|
||||||
|
showSearchTypeDropdown = false;
|
||||||
|
};
|
||||||
|
|
||||||
const onsubmit = (event: Event) => {
|
const onsubmit = (event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
onSubmit();
|
onSubmit();
|
||||||
@@ -180,17 +201,18 @@
|
|||||||
case 'metadata':
|
case 'metadata':
|
||||||
case 'description':
|
case 'description':
|
||||||
case 'ocr': {
|
case 'ocr': {
|
||||||
|
currentSearchType = searchType;
|
||||||
return searchType;
|
return searchType;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
currentSearchType = 'smart';
|
||||||
return 'smart';
|
return 'smart';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSearchTypeText(): string {
|
function getSearchTypeText(): string {
|
||||||
const searchType = getSearchType();
|
switch (currentSearchType) {
|
||||||
switch (searchType) {
|
|
||||||
case 'smart': {
|
case 'smart': {
|
||||||
return $t('context');
|
return $t('context');
|
||||||
}
|
}
|
||||||
@@ -203,8 +225,22 @@
|
|||||||
case 'ocr': {
|
case 'ocr': {
|
||||||
return $t('ocr');
|
return $t('ocr');
|
||||||
}
|
}
|
||||||
|
default: {
|
||||||
|
return $t('context');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
getSearchType();
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchTypes = [
|
||||||
|
{ value: 'smart', label: () => $t('context') },
|
||||||
|
{ value: 'metadata', label: () => $t('filename') },
|
||||||
|
{ value: 'description', label: () => $t('description') },
|
||||||
|
{ value: 'ocr', label: () => $t('ocr') },
|
||||||
|
] as const;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:document
|
<svelte:document
|
||||||
@@ -293,11 +329,34 @@
|
|||||||
class:max-md:hidden={value}
|
class:max-md:hidden={value}
|
||||||
class:end-28={value.length > 0}
|
class:end-28={value.length > 0}
|
||||||
>
|
>
|
||||||
<p
|
<div class="relative">
|
||||||
class="bg-immich-primary text-white dark:bg-immich-dark-primary/90 dark:text-black/75 rounded-full px-3 py-1 text-xs"
|
<Button
|
||||||
|
class="bg-immich-primary text-white dark:bg-immich-dark-primary/90 dark:text-black/75 rounded-full px-3 py-1 text-xs hover:opacity-80 transition-opacity cursor-pointer"
|
||||||
|
onclick={toggleSearchTypeDropdown}
|
||||||
|
aria-expanded={showSearchTypeDropdown}
|
||||||
|
aria-haspopup="listbox"
|
||||||
>
|
>
|
||||||
{getSearchTypeText()}
|
{getSearchTypeText()}
|
||||||
</p>
|
</Button>
|
||||||
|
|
||||||
|
{#if showSearchTypeDropdown}
|
||||||
|
<div
|
||||||
|
class="absolute top-full right-0 mt-1 bg-white dark:bg-immich-dark-gray border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg py-1 min-w-32 z-9999"
|
||||||
|
use:focusOutside={{ onFocusOut: closeSearchTypeDropdown }}
|
||||||
|
>
|
||||||
|
{#each searchTypes as searchType (searchType.value)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-full text-left px-3 py-2 text-xs hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors
|
||||||
|
{currentSearchType === searchType.value ? 'bg-gray-100 dark:bg-gray-700' : ''}"
|
||||||
|
onclick={() => selectSearchType(searchType.value)}
|
||||||
|
>
|
||||||
|
{searchType.label()}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export enum AppRoute {
|
|||||||
ADMIN_LIBRARY_MANAGEMENT = '/admin/library-management',
|
ADMIN_LIBRARY_MANAGEMENT = '/admin/library-management',
|
||||||
ADMIN_SETTINGS = '/admin/system-settings',
|
ADMIN_SETTINGS = '/admin/system-settings',
|
||||||
ADMIN_STATS = '/admin/server-status',
|
ADMIN_STATS = '/admin/server-status',
|
||||||
ADMIN_JOBS = '/admin/jobs-status',
|
ADMIN_QUEUES = '/admin/queues',
|
||||||
ADMIN_REPAIR = '/admin/repair',
|
ADMIN_REPAIR = '/admin/repair',
|
||||||
|
|
||||||
ALBUMS = '/albums',
|
ALBUMS = '/albums',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type {
|
|||||||
AlbumResponseDto,
|
AlbumResponseDto,
|
||||||
LibraryResponseDto,
|
LibraryResponseDto,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
|
QueueResponseDto,
|
||||||
SharedLinkResponseDto,
|
SharedLinkResponseDto,
|
||||||
SystemConfigDto,
|
SystemConfigDto,
|
||||||
UserAdminResponseDto,
|
UserAdminResponseDto,
|
||||||
@@ -21,6 +22,8 @@ export type Events = {
|
|||||||
|
|
||||||
AlbumDelete: [AlbumResponseDto];
|
AlbumDelete: [AlbumResponseDto];
|
||||||
|
|
||||||
|
QueueUpdate: [QueueResponseDto];
|
||||||
|
|
||||||
SharedLinkCreate: [SharedLinkResponseDto];
|
SharedLinkCreate: [SharedLinkResponseDto];
|
||||||
SharedLinkUpdate: [SharedLinkResponseDto];
|
SharedLinkUpdate: [SharedLinkResponseDto];
|
||||||
SharedLinkDelete: [SharedLinkResponseDto];
|
SharedLinkDelete: [SharedLinkResponseDto];
|
||||||
|
|||||||
45
web/src/lib/managers/queue-manager.svelte.ts
Normal file
45
web/src/lib/managers/queue-manager.svelte.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||||
|
import type { QueueSnapshot } from '$lib/types';
|
||||||
|
import { getQueues, type QueueResponseDto } from '@immich/sdk';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
|
export class QueueManager {
|
||||||
|
#snapshots = $state<QueueSnapshot[]>([]);
|
||||||
|
#queues: QueueResponseDto[] = $derived(this.#snapshots.at(-1)?.snapshot ?? []);
|
||||||
|
|
||||||
|
#interval?: ReturnType<typeof setInterval>;
|
||||||
|
#listenerCount = 0;
|
||||||
|
|
||||||
|
get snapshots() {
|
||||||
|
return this.#snapshots;
|
||||||
|
}
|
||||||
|
|
||||||
|
get queues() {
|
||||||
|
return this.#queues;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
eventManager.on('QueueUpdate', () => void this.refresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
listen() {
|
||||||
|
if (!this.#interval) {
|
||||||
|
this.#interval = setInterval(() => void this.refresh(true), 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#listenerCount++;
|
||||||
|
void this.refresh();
|
||||||
|
|
||||||
|
return () => this.#listenerCount--;
|
||||||
|
}
|
||||||
|
|
||||||
|
async refresh(tick = false) {
|
||||||
|
this.#snapshots.push({
|
||||||
|
timestamp: DateTime.now().toMillis(),
|
||||||
|
snapshot: this.#listenerCount > 0 || !tick ? await getQueues().catch(() => undefined) : undefined,
|
||||||
|
});
|
||||||
|
this.#snapshots = this.#snapshots.slice(-30);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const queueManager = new QueueManager();
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
|
|
||||||
<Text size="small" class="mt-2" color="muted">
|
<Text size="small" class="mt-2" color="muted">
|
||||||
{$t('admin.note_apply_storage_label_previous_assets')}
|
{$t('admin.note_apply_storage_label_previous_assets')}
|
||||||
<Link href={AppRoute.ADMIN_JOBS}>
|
<Link href={AppRoute.ADMIN_QUEUES}>
|
||||||
{$t('admin.storage_template_migration_job')}
|
{$t('admin.storage_template_migration_job')}
|
||||||
</Link>
|
</Link>
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
246
web/src/lib/services/queue.service.ts
Normal file
246
web/src/lib/services/queue.service.ts
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { AppRoute } from '$lib/constants';
|
||||||
|
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||||
|
import JobCreateModal from '$lib/modals/JobCreateModal.svelte';
|
||||||
|
import { user } from '$lib/stores/user.store';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
|
import { emptyQueue, getQueue, QueueName, updateQueue, type QueueResponseDto } from '@immich/sdk';
|
||||||
|
import { modalManager, toastManager, type ActionItem, type IconLike } from '@immich/ui';
|
||||||
|
import {
|
||||||
|
mdiClose,
|
||||||
|
mdiCog,
|
||||||
|
mdiContentDuplicate,
|
||||||
|
mdiDatabaseOutline,
|
||||||
|
mdiFaceRecognition,
|
||||||
|
mdiFileJpgBox,
|
||||||
|
mdiFileXmlBox,
|
||||||
|
mdiFolderMove,
|
||||||
|
mdiImageSearch,
|
||||||
|
mdiLibraryShelves,
|
||||||
|
mdiOcr,
|
||||||
|
mdiPause,
|
||||||
|
mdiPlay,
|
||||||
|
mdiPlus,
|
||||||
|
mdiStateMachine,
|
||||||
|
mdiSync,
|
||||||
|
mdiTable,
|
||||||
|
mdiTagFaces,
|
||||||
|
mdiTrashCanOutline,
|
||||||
|
mdiTrayFull,
|
||||||
|
mdiVideo,
|
||||||
|
} from '@mdi/js';
|
||||||
|
import type { MessageFormatter } from 'svelte-i18n';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
|
type QueueItem = {
|
||||||
|
icon: IconLike;
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getQueuesActions = ($t: MessageFormatter) => {
|
||||||
|
const ViewQueues: ActionItem = {
|
||||||
|
title: $t('admin.queues'),
|
||||||
|
description: $t('admin.queues_page_description'),
|
||||||
|
icon: mdiSync,
|
||||||
|
type: $t('page'),
|
||||||
|
isGlobal: true,
|
||||||
|
$if: () => get(user)?.isAdmin,
|
||||||
|
onAction: () => goto(AppRoute.ADMIN_QUEUES),
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreateJob: ActionItem = {
|
||||||
|
icon: mdiPlus,
|
||||||
|
title: $t('admin.create_job'),
|
||||||
|
type: $t('command'),
|
||||||
|
shortcuts: { shift: true, key: 'n' },
|
||||||
|
onAction: async () => {
|
||||||
|
await modalManager.show(JobCreateModal, {});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ManageConcurrency: ActionItem = {
|
||||||
|
icon: mdiCog,
|
||||||
|
title: $t('admin.manage_concurrency'),
|
||||||
|
description: $t('admin.manage_concurrency_description'),
|
||||||
|
type: $t('page'),
|
||||||
|
onAction: () => goto(`${AppRoute.ADMIN_SETTINGS}?isOpen=job`),
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ViewQueues, ManageConcurrency, CreateJob };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getQueueActions = ($t: MessageFormatter, queue: QueueResponseDto) => {
|
||||||
|
const Pause: ActionItem = {
|
||||||
|
icon: mdiPause,
|
||||||
|
title: $t('pause'),
|
||||||
|
$if: () => !queue.isPaused,
|
||||||
|
onAction: () => handlePauseQueue(queue),
|
||||||
|
};
|
||||||
|
|
||||||
|
const Resume: ActionItem = {
|
||||||
|
icon: mdiPlay,
|
||||||
|
title: $t('resume'),
|
||||||
|
$if: () => queue.isPaused,
|
||||||
|
onAction: () => handleResumeQueue(queue),
|
||||||
|
};
|
||||||
|
|
||||||
|
const Empty: ActionItem = {
|
||||||
|
icon: mdiClose,
|
||||||
|
title: $t('clear'),
|
||||||
|
onAction: () => handleEmptyQueue(queue),
|
||||||
|
};
|
||||||
|
|
||||||
|
const RemoveFailedJobs: ActionItem = {
|
||||||
|
icon: mdiTrashCanOutline,
|
||||||
|
color: 'danger',
|
||||||
|
title: $t('admin.remove_failed_jobs'),
|
||||||
|
onAction: () => handleRemoveFailedJobs(queue),
|
||||||
|
};
|
||||||
|
|
||||||
|
return { Pause, Resume, Empty, RemoveFailedJobs };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handlePauseQueue = async (queue: QueueResponseDto) => {
|
||||||
|
const response = await updateQueue({ name: queue.name, queueUpdateDto: { isPaused: true } });
|
||||||
|
eventManager.emit('QueueUpdate', response);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleResumeQueue = async (queue: QueueResponseDto) => {
|
||||||
|
const response = await updateQueue({ name: queue.name, queueUpdateDto: { isPaused: false } });
|
||||||
|
eventManager.emit('QueueUpdate', response);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleEmptyQueue = async (queue: QueueResponseDto) => {
|
||||||
|
const $t = await getFormatter();
|
||||||
|
const item = asQueueItem($t, queue);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await emptyQueue({ name: queue.name, queueDeleteDto: { failed: false } });
|
||||||
|
const response = await getQueue({ name: queue.name });
|
||||||
|
eventManager.emit('QueueUpdate', response);
|
||||||
|
toastManager.success($t('admin.cleared_jobs', { values: { job: item.title } }));
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.something_went_wrong'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFailedJobs = async (queue: QueueResponseDto) => {
|
||||||
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await emptyQueue({ name: queue.name, queueDeleteDto: { failed: true } });
|
||||||
|
const response = await getQueue({ name: queue.name });
|
||||||
|
eventManager.emit('QueueUpdate', response);
|
||||||
|
toastManager.success();
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.something_went_wrong'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const asQueueItem = ($t: MessageFormatter, queue: { name: QueueName }): QueueItem => {
|
||||||
|
// TODO merge this mapping with data from QueuePanel.svelte
|
||||||
|
const items: Record<QueueName, QueueItem> = {
|
||||||
|
[QueueName.ThumbnailGeneration]: {
|
||||||
|
icon: mdiFileJpgBox,
|
||||||
|
title: $t('admin.thumbnail_generation_job'),
|
||||||
|
subtitle: $t('admin.thumbnail_generation_job_description'),
|
||||||
|
},
|
||||||
|
[QueueName.MetadataExtraction]: {
|
||||||
|
icon: mdiTable,
|
||||||
|
title: $t('admin.metadata_extraction_job'),
|
||||||
|
subtitle: $t('admin.metadata_extraction_job_description'),
|
||||||
|
},
|
||||||
|
[QueueName.Library]: {
|
||||||
|
icon: mdiLibraryShelves,
|
||||||
|
title: $t('external_libraries'),
|
||||||
|
subtitle: $t('admin.library_tasks_description'),
|
||||||
|
},
|
||||||
|
[QueueName.Sidecar]: {
|
||||||
|
title: $t('admin.sidecar_job'),
|
||||||
|
icon: mdiFileXmlBox,
|
||||||
|
subtitle: $t('admin.sidecar_job_description'),
|
||||||
|
},
|
||||||
|
[QueueName.SmartSearch]: {
|
||||||
|
icon: mdiImageSearch,
|
||||||
|
title: $t('admin.machine_learning_smart_search'),
|
||||||
|
subtitle: $t('admin.smart_search_job_description'),
|
||||||
|
},
|
||||||
|
[QueueName.DuplicateDetection]: {
|
||||||
|
icon: mdiContentDuplicate,
|
||||||
|
title: $t('admin.machine_learning_duplicate_detection'),
|
||||||
|
subtitle: $t('admin.duplicate_detection_job_description'),
|
||||||
|
},
|
||||||
|
[QueueName.FaceDetection]: {
|
||||||
|
icon: mdiFaceRecognition,
|
||||||
|
title: $t('admin.face_detection'),
|
||||||
|
subtitle: $t('admin.face_detection_description'),
|
||||||
|
},
|
||||||
|
[QueueName.FacialRecognition]: {
|
||||||
|
icon: mdiTagFaces,
|
||||||
|
title: $t('admin.machine_learning_facial_recognition'),
|
||||||
|
subtitle: $t('admin.facial_recognition_job_description'),
|
||||||
|
},
|
||||||
|
[QueueName.Ocr]: {
|
||||||
|
icon: mdiOcr,
|
||||||
|
title: $t('admin.machine_learning_ocr'),
|
||||||
|
subtitle: $t('admin.ocr_job_description'),
|
||||||
|
},
|
||||||
|
[QueueName.VideoConversion]: {
|
||||||
|
icon: mdiVideo,
|
||||||
|
title: $t('admin.video_conversion_job'),
|
||||||
|
subtitle: $t('admin.video_conversion_job_description'),
|
||||||
|
},
|
||||||
|
[QueueName.StorageTemplateMigration]: {
|
||||||
|
icon: mdiFolderMove,
|
||||||
|
title: $t('admin.storage_template_migration'),
|
||||||
|
},
|
||||||
|
[QueueName.Migration]: {
|
||||||
|
icon: mdiFolderMove,
|
||||||
|
title: $t('admin.migration_job'),
|
||||||
|
subtitle: $t('admin.migration_job_description'),
|
||||||
|
},
|
||||||
|
[QueueName.BackgroundTask]: {
|
||||||
|
icon: mdiTrayFull,
|
||||||
|
title: $t('admin.background_task_job'),
|
||||||
|
},
|
||||||
|
[QueueName.Search]: {
|
||||||
|
icon: '',
|
||||||
|
title: $t('search'),
|
||||||
|
},
|
||||||
|
[QueueName.Notifications]: {
|
||||||
|
icon: '',
|
||||||
|
title: $t('notifications'),
|
||||||
|
},
|
||||||
|
[QueueName.BackupDatabase]: {
|
||||||
|
icon: mdiDatabaseOutline,
|
||||||
|
title: $t('admin.backup_database'),
|
||||||
|
},
|
||||||
|
[QueueName.Workflow]: {
|
||||||
|
icon: mdiStateMachine,
|
||||||
|
title: $t('workflow'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return items[queue.name];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const asQueueSlug = (name: QueueName) => {
|
||||||
|
return name.replaceAll(/[A-Z]/g, (m) => '-' + m.toLowerCase());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fromQueueSlug = (slug: string): QueueName | undefined => {
|
||||||
|
const name = slug.replaceAll(/-([a-z])/g, (_, c) => c.toUpperCase());
|
||||||
|
if (Object.values(QueueName).includes(name as QueueName)) {
|
||||||
|
return name as QueueName;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getQueueDetailUrl = (queue: QueueResponseDto) => {
|
||||||
|
return `${AppRoute.ADMIN_QUEUES}/${asQueueSlug(queue.name)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleViewQueue = (queue: QueueResponseDto) => {
|
||||||
|
return goto(getQueueDetailUrl(queue));
|
||||||
|
};
|
||||||
@@ -2,16 +2,16 @@
|
|||||||
import BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte';
|
import BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import { NavbarItem } from '@immich/ui';
|
import { NavbarItem } from '@immich/ui';
|
||||||
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync } from '@mdi/js';
|
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiTrayFull } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="h-full flex flex-col justify-between gap-2">
|
<div class="h-full flex flex-col justify-between gap-2">
|
||||||
<div class="flex flex-col pt-8 pe-4 gap-1">
|
<div class="flex flex-col pt-8 pe-4 gap-1">
|
||||||
<NavbarItem title={$t('users')} href={AppRoute.ADMIN_USERS} icon={mdiAccountMultipleOutline} />
|
<NavbarItem title={$t('users')} href={AppRoute.ADMIN_USERS} icon={mdiAccountMultipleOutline} />
|
||||||
<NavbarItem title={$t('jobs')} href={AppRoute.ADMIN_JOBS} icon={mdiSync} />
|
|
||||||
<NavbarItem title={$t('settings')} href={AppRoute.ADMIN_SETTINGS} icon={mdiCog} />
|
|
||||||
<NavbarItem title={$t('external_libraries')} href={AppRoute.ADMIN_LIBRARY_MANAGEMENT} icon={mdiBookshelf} />
|
<NavbarItem title={$t('external_libraries')} href={AppRoute.ADMIN_LIBRARY_MANAGEMENT} icon={mdiBookshelf} />
|
||||||
|
<NavbarItem title={$t('admin.queues')} href={AppRoute.ADMIN_QUEUES} icon={mdiTrayFull} />
|
||||||
|
<NavbarItem title={$t('settings')} href={AppRoute.ADMIN_SETTINGS} icon={mdiCog} />
|
||||||
<NavbarItem title={$t('server_stats')} href={AppRoute.ADMIN_STATS} icon={mdiServer} />
|
<NavbarItem title={$t('server_stats')} href={AppRoute.ADMIN_STATS} icon={mdiServer} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ServerVersionResponseDto } from '@immich/sdk';
|
import type { QueueResponseDto, ServerVersionResponseDto } from '@immich/sdk';
|
||||||
|
|
||||||
export interface ReleaseEvent {
|
export interface ReleaseEvent {
|
||||||
isAvailable: boolean;
|
isAvailable: boolean;
|
||||||
@@ -7,3 +7,5 @@ export interface ReleaseEvent {
|
|||||||
serverVersion: ServerVersionResponseDto;
|
serverVersion: ServerVersionResponseDto;
|
||||||
releaseVersion: ServerVersionResponseDto;
|
releaseVersion: ServerVersionResponseDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type QueueSnapshot = { timestamp: number; snapshot?: QueueResponseDto[] };
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
||||||
import ServerRestartingModal from '$lib/modals/ServerRestartingModal.svelte';
|
import ServerRestartingModal from '$lib/modals/ServerRestartingModal.svelte';
|
||||||
import VersionAnnouncementModal from '$lib/modals/VersionAnnouncementModal.svelte';
|
import VersionAnnouncementModal from '$lib/modals/VersionAnnouncementModal.svelte';
|
||||||
|
import { getQueuesActions } from '$lib/services/queue.service';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import { closeWebsocketConnection, openWebsocketConnection, websocketStore } from '$lib/stores/websocket';
|
import { closeWebsocketConnection, openWebsocketConnection, websocketStore } from '$lib/stores/websocket';
|
||||||
import type { ReleaseEvent } from '$lib/types';
|
import type { ReleaseEvent } from '$lib/types';
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
import { maintenanceShouldRedirect } from '$lib/utils/maintenance';
|
import { maintenanceShouldRedirect } from '$lib/utils/maintenance';
|
||||||
import { isAssetViewerRoute } from '$lib/utils/navigation';
|
import { isAssetViewerRoute } from '$lib/utils/navigation';
|
||||||
import { CommandPaletteContext, modalManager, setTranslations, type ActionItem } from '@immich/ui';
|
import { CommandPaletteContext, modalManager, setTranslations, type ActionItem } from '@immich/ui';
|
||||||
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync, mdiThemeLightDark } from '@mdi/js';
|
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiThemeLightDark } from '@mdi/js';
|
||||||
import { onMount, type Snippet } from 'svelte';
|
import { onMount, type Snippet } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
@@ -142,15 +143,9 @@
|
|||||||
icon: mdiAccountMultipleOutline,
|
icon: mdiAccountMultipleOutline,
|
||||||
onAction: () => goto(AppRoute.ADMIN_USERS),
|
onAction: () => goto(AppRoute.ADMIN_USERS),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: $t('jobs'),
|
|
||||||
description: $t('admin.jobs_page_description'),
|
|
||||||
icon: mdiSync,
|
|
||||||
onAction: () => goto(AppRoute.ADMIN_JOBS),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: $t('settings'),
|
title: $t('settings'),
|
||||||
description: $t('admin.jobs_page_description'),
|
description: $t('admin.settings_page_description'),
|
||||||
icon: mdiCog,
|
icon: mdiCog,
|
||||||
onAction: () => goto(AppRoute.ADMIN_SETTINGS),
|
onAction: () => goto(AppRoute.ADMIN_SETTINGS),
|
||||||
},
|
},
|
||||||
@@ -168,7 +163,7 @@
|
|||||||
},
|
},
|
||||||
].map((route) => ({ ...route, type: $t('page'), isGlobal: true, $if: () => $user?.isAdmin }));
|
].map((route) => ({ ...route, type: $t('page'), isGlobal: true, $if: () => $user?.isAdmin }));
|
||||||
|
|
||||||
const commands = $derived([...userCommands, ...adminCommands]);
|
const commands = $derived([...userCommands, ...adminCommands, ...Object.values(getQueuesActions($t))]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<OnEvents {onReleaseEvent} />
|
<OnEvents {onReleaseEvent} />
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import JobsPanel from '$lib/components/jobs/JobsPanel.svelte';
|
|
||||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
|
||||||
import { AppRoute } from '$lib/constants';
|
|
||||||
import JobCreateModal from '$lib/modals/JobCreateModal.svelte';
|
|
||||||
import { asyncTimeout } from '$lib/utils';
|
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
|
||||||
import {
|
|
||||||
getQueuesLegacy,
|
|
||||||
QueueCommand,
|
|
||||||
QueueName,
|
|
||||||
runQueueCommandLegacy,
|
|
||||||
type QueuesResponseLegacyDto,
|
|
||||||
} from '@immich/sdk';
|
|
||||||
import { Button, CommandPaletteContext, HStack, modalManager, Text, type ActionItem } from '@immich/ui';
|
|
||||||
import { mdiCog, mdiPlay, mdiPlus } from '@mdi/js';
|
|
||||||
import { onDestroy, onMount } from 'svelte';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
import type { PageData } from './$types';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
data: PageData;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { data }: Props = $props();
|
|
||||||
|
|
||||||
let jobs: QueuesResponseLegacyDto | undefined = $state();
|
|
||||||
|
|
||||||
let running = true;
|
|
||||||
|
|
||||||
const pausedJobs = $derived(
|
|
||||||
Object.entries(jobs ?? {})
|
|
||||||
.filter(([_, queue]) => queue.queueStatus?.isPaused)
|
|
||||||
.map(([name]) => name as QueueName),
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleResumePausedJobs = async () => {
|
|
||||||
try {
|
|
||||||
for (const name of pausedJobs) {
|
|
||||||
await runQueueCommandLegacy({ name, queueCommandDto: { command: QueueCommand.Resume, force: false } });
|
|
||||||
}
|
|
||||||
// Refresh jobs status immediately after resuming
|
|
||||||
jobs = await getQueuesLegacy();
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('admin.failed_job_command', { values: { command: 'resume', job: 'paused jobs' } }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateJob = () => modalManager.show(JobCreateModal);
|
|
||||||
|
|
||||||
const jobConcurrencyLink = `${AppRoute.ADMIN_SETTINGS}?isOpen=job`;
|
|
||||||
|
|
||||||
const commands: ActionItem[] = [
|
|
||||||
{
|
|
||||||
title: $t('admin.create_job'),
|
|
||||||
type: $t('command'),
|
|
||||||
icon: mdiPlus,
|
|
||||||
onAction: () => void handleCreateJob(),
|
|
||||||
shortcuts: { shift: true, key: 'n' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: $t('admin.manage_concurrency'),
|
|
||||||
description: $t('admin.manage_concurrency_description'),
|
|
||||||
type: $t('page'),
|
|
||||||
icon: mdiCog,
|
|
||||||
onAction: () => goto(jobConcurrencyLink),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
while (running) {
|
|
||||||
jobs = await getQueuesLegacy();
|
|
||||||
await asyncTimeout(5000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
running = false;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<CommandPaletteContext {commands} />
|
|
||||||
|
|
||||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
|
|
||||||
{#snippet buttons()}
|
|
||||||
<HStack gap={0}>
|
|
||||||
{#if pausedJobs.length > 0}
|
|
||||||
<Button
|
|
||||||
leadingIcon={mdiPlay}
|
|
||||||
onclick={handleResumePausedJobs}
|
|
||||||
size="small"
|
|
||||||
variant="ghost"
|
|
||||||
title={pausedJobs.join(', ')}
|
|
||||||
>
|
|
||||||
<Text class="hidden md:block">
|
|
||||||
{$t('resume_paused_jobs', { values: { count: pausedJobs.length } })}
|
|
||||||
</Text>
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
<Button leadingIcon={mdiPlus} onclick={handleCreateJob} size="small" variant="ghost" color="secondary">
|
|
||||||
<Text class="hidden md:block">{$t('admin.create_job')}</Text>
|
|
||||||
</Button>
|
|
||||||
<Button leadingIcon={mdiCog} href={jobConcurrencyLink} size="small" variant="ghost" color="secondary">
|
|
||||||
<Text class="hidden md:block">{$t('admin.manage_concurrency')}</Text>
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
{/snippet}
|
|
||||||
<section id="setting-content" class="flex place-content-center sm:mx-4">
|
|
||||||
<section class="w-full pb-28 sm:w-5/6 md:w-212.5">
|
|
||||||
{#if jobs}
|
|
||||||
<JobsPanel {jobs} />
|
|
||||||
{/if}
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
</AdminPageLayout>
|
|
||||||
@@ -1,18 +1,5 @@
|
|||||||
import { authenticate } from '$lib/utils/auth';
|
import { AppRoute } from '$lib/constants';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { redirect } from '@sveltejs/kit';
|
||||||
import { getQueuesLegacy } from '@immich/sdk';
|
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async ({ url }) => {
|
export const load = (() => redirect(307, AppRoute.ADMIN_QUEUES)) satisfies PageLoad;
|
||||||
await authenticate(url, { admin: true });
|
|
||||||
|
|
||||||
const jobs = await getQueuesLegacy();
|
|
||||||
const $t = await getFormatter();
|
|
||||||
|
|
||||||
return {
|
|
||||||
jobs,
|
|
||||||
meta: {
|
|
||||||
title: $t('admin.job_status'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}) satisfies PageLoad;
|
|
||||||
|
|||||||
83
web/src/routes/admin/queues/+page.svelte
Normal file
83
web/src/routes/admin/queues/+page.svelte
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import HeaderButton from '$lib/components/HeaderButton.svelte';
|
||||||
|
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||||
|
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||||
|
import JobsPanel from '$lib/components/QueuePanel.svelte';
|
||||||
|
import { queueManager } from '$lib/managers/queue-manager.svelte';
|
||||||
|
import { getQueuesActions } from '$lib/services/queue.service';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import { QueueCommand, runQueueCommandLegacy, type QueueResponseDto } from '@immich/sdk';
|
||||||
|
import { Button, CommandPaletteContext, HStack, Text, type ActionItem } from '@immich/ui';
|
||||||
|
import { mdiPlay } from '@mdi/js';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: PageData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data }: Props = $props();
|
||||||
|
|
||||||
|
onMount(() => queueManager.listen());
|
||||||
|
|
||||||
|
let queues = $derived<QueueResponseDto[]>(queueManager.queues);
|
||||||
|
const pausedQueues = $derived(queues.filter(({ isPaused }) => isPaused).map(({ name }) => name));
|
||||||
|
|
||||||
|
const handleResumePausedJobs = async () => {
|
||||||
|
try {
|
||||||
|
for (const name of pausedQueues) {
|
||||||
|
await runQueueCommandLegacy({ name, queueCommandDto: { command: QueueCommand.Resume, force: false } });
|
||||||
|
}
|
||||||
|
await queueManager.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('admin.failed_job_command', { values: { command: 'resume', job: 'paused jobs' } }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { CreateJob, ManageConcurrency } = $derived(getQueuesActions($t));
|
||||||
|
const commands: ActionItem[] = $derived([CreateJob, ManageConcurrency]);
|
||||||
|
|
||||||
|
const onQueueUpdate = (update: QueueResponseDto) => {
|
||||||
|
queues = queues.map((queue) => {
|
||||||
|
if (queue.name === update.name) {
|
||||||
|
return update;
|
||||||
|
}
|
||||||
|
return queue;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CommandPaletteContext {commands} />
|
||||||
|
|
||||||
|
<OnEvents {onQueueUpdate} />
|
||||||
|
|
||||||
|
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
|
||||||
|
{#snippet buttons()}
|
||||||
|
<HStack gap={0}>
|
||||||
|
{#if pausedQueues.length > 0}
|
||||||
|
<Button
|
||||||
|
leadingIcon={mdiPlay}
|
||||||
|
onclick={handleResumePausedJobs}
|
||||||
|
size="small"
|
||||||
|
variant="ghost"
|
||||||
|
title={pausedQueues.join(', ')}
|
||||||
|
>
|
||||||
|
<Text class="hidden md:block">
|
||||||
|
{$t('resume_paused_jobs', { values: { count: pausedQueues.length } })}
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
<HeaderButton action={CreateJob} />
|
||||||
|
<HeaderButton action={ManageConcurrency} />
|
||||||
|
</HStack>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<section id="setting-content" class="flex place-content-center sm:mx-4">
|
||||||
|
<section class="w-full pb-28 sm:w-5/6 md:w-212.5">
|
||||||
|
{#if queues}
|
||||||
|
<JobsPanel {queues} />
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</AdminPageLayout>
|
||||||
18
web/src/routes/admin/queues/+page.ts
Normal file
18
web/src/routes/admin/queues/+page.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { authenticate } from '$lib/utils/auth';
|
||||||
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
|
import { getQueues } from '@immich/sdk';
|
||||||
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
|
export const load = (async ({ url }) => {
|
||||||
|
await authenticate(url, { admin: true });
|
||||||
|
|
||||||
|
const queues = await getQueues();
|
||||||
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
return {
|
||||||
|
queues,
|
||||||
|
meta: {
|
||||||
|
title: $t('admin.queues'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}) satisfies PageLoad;
|
||||||
82
web/src/routes/admin/queues/[name]/+page.svelte
Normal file
82
web/src/routes/admin/queues/[name]/+page.svelte
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import HeaderButton from '$lib/components/HeaderButton.svelte';
|
||||||
|
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||||
|
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||||
|
import QueueGraph from '$lib/components/QueueGraph.svelte';
|
||||||
|
import { AppRoute } from '$lib/constants';
|
||||||
|
import { queueManager } from '$lib/managers/queue-manager.svelte';
|
||||||
|
import { asQueueItem, getQueueActions } from '$lib/services/queue.service';
|
||||||
|
import { type QueueResponseDto } from '@immich/sdk';
|
||||||
|
import { Badge, Card, CardBody, CardHeader, CardTitle, Container, Heading, HStack, Icon, Text } from '@immich/ui';
|
||||||
|
import { mdiClockTimeTwoOutline } from '@mdi/js';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: PageData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data }: Props = $props();
|
||||||
|
|
||||||
|
let queue = $derived(data.queue);
|
||||||
|
|
||||||
|
const { Pause, Resume, Empty, RemoveFailedJobs } = $derived(getQueueActions($t, queue));
|
||||||
|
const item = $derived(asQueueItem($t, queue));
|
||||||
|
|
||||||
|
onMount(() => queueManager.listen());
|
||||||
|
|
||||||
|
const onQueueUpdate = (update: QueueResponseDto) => {
|
||||||
|
if (update.name === queue.name) {
|
||||||
|
queue = update;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<OnEvents {onQueueUpdate} />
|
||||||
|
|
||||||
|
<AdminPageLayout breadcrumbs={[{ title: $t('admin.queues'), href: AppRoute.ADMIN_QUEUES }, { title: item.title }]}>
|
||||||
|
{#snippet buttons()}
|
||||||
|
<HStack gap={0}>
|
||||||
|
<HeaderButton action={Pause} />
|
||||||
|
<HeaderButton action={Resume} />
|
||||||
|
<HeaderButton action={Empty} />
|
||||||
|
<HeaderButton action={RemoveFailedJobs} />
|
||||||
|
</HStack>
|
||||||
|
{/snippet}
|
||||||
|
<div>
|
||||||
|
<Container size="large" center>
|
||||||
|
<div class="mb-1 mt-4 flex items-center gap-2">
|
||||||
|
<Heading tag="h1" size="large">{item.title}</Heading>
|
||||||
|
{#if queue.isPaused}
|
||||||
|
<Badge color="warning">
|
||||||
|
{$t('paused')}
|
||||||
|
</Badge>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<Text color="muted" class="mb-4">{item.subtitle}</Text>
|
||||||
|
|
||||||
|
<div class="flex gap-1 mb-4">
|
||||||
|
<Badge>{$t('active_count', { values: { count: queue.statistics.active } })}</Badge>
|
||||||
|
<Badge>{$t('waiting_count', { values: { count: queue.statistics.waiting } })}</Badge>
|
||||||
|
{#if queue.statistics.failed > 0}
|
||||||
|
<Badge color="danger">{$t('failed_count', { values: { count: queue.statistics.failed } })}</Badge>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8">
|
||||||
|
<Card color="secondary">
|
||||||
|
<CardHeader>
|
||||||
|
<div class="flex items-center gap-2 text-primary">
|
||||||
|
<Icon icon={mdiClockTimeTwoOutline} size="1.5rem" />
|
||||||
|
<CardTitle>{$t('admin.jobs_over_time')}</CardTitle>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody>
|
||||||
|
<QueueGraph {queue} class="h-[300px]" />
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
</AdminPageLayout>
|
||||||
31
web/src/routes/admin/queues/[name]/+page.ts
Normal file
31
web/src/routes/admin/queues/[name]/+page.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { AppRoute } from '$lib/constants';
|
||||||
|
import { fromQueueSlug } from '$lib/services/queue.service';
|
||||||
|
import { authenticate, requestServerInfo } from '$lib/utils/auth';
|
||||||
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
|
import { getQueue, getQueueJobs, QueueJobStatus } from '@immich/sdk';
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
|
export const load = (async ({ params, url }) => {
|
||||||
|
await authenticate(url, { admin: true });
|
||||||
|
await requestServerInfo();
|
||||||
|
|
||||||
|
const name = fromQueueSlug(params.name);
|
||||||
|
if (!name) {
|
||||||
|
redirect(302, AppRoute.ADMIN_QUEUES);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [queue, failedJobs] = await Promise.all([
|
||||||
|
getQueue({ name }),
|
||||||
|
getQueueJobs({ name, status: [QueueJobStatus.Failed, QueueJobStatus.Paused] }),
|
||||||
|
]);
|
||||||
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
return {
|
||||||
|
queue,
|
||||||
|
failedJobs,
|
||||||
|
meta: {
|
||||||
|
title: $t('admin.queue_details'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}) satisfies PageLoad;
|
||||||
Reference in New Issue
Block a user