Compare commits
28 Commits
v1.105.0
...
feat/serve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea33cd9515 | ||
|
|
e9ceb8b017 | ||
|
|
94030d809f | ||
|
|
4db0717a08 | ||
|
|
b6808c1675 | ||
|
|
a2b7403978 | ||
|
|
1556d978ed | ||
|
|
48a71ac4d9 | ||
|
|
adf620331c | ||
|
|
c92637df80 | ||
|
|
3356b023c5 | ||
|
|
a2e8b657e6 | ||
|
|
f420befc15 | ||
|
|
efb844c6cd | ||
|
|
88d4338348 | ||
|
|
ce7bbe88f9 | ||
|
|
d62e90424e | ||
|
|
0f129cae4a | ||
|
|
5583635947 | ||
|
|
b7715305b3 | ||
|
|
6c008176c9 | ||
|
|
72b1d582ba | ||
|
|
7b1112f3e3 | ||
|
|
42d0fc85ca | ||
|
|
d32b621750 | ||
|
|
d551003311 | ||
|
|
dd64379a4e | ||
|
|
c1cdda0ac4 |
1
base-images
Submodule
1
base-images
Submodule
Submodule base-images added at d0d3ab018c
2
cli/package-lock.json
generated
2
cli/package-lock.json
generated
@@ -47,7 +47,7 @@
|
||||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.105.0",
|
||||
"version": "1.105.1",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
version: "3.8"
|
||||
|
||||
# Configurations for hardware-accelerated machine learning
|
||||
|
||||
# If using Unraid or another platform that doesn't allow multiple Compose files,
|
||||
# you can inline the config for a backend by copying its contents
|
||||
# you can inline the config for a backend by copying its contents
|
||||
# into the immich-machine-learning service in the docker-compose.yml file.
|
||||
|
||||
# See https://immich.app/docs/features/ml-hardware-acceleration for info on usage.
|
||||
@@ -30,7 +28,7 @@ services:
|
||||
|
||||
openvino:
|
||||
device_cgroup_rules:
|
||||
- "c 189:* rmw"
|
||||
- 'c 189:* rmw'
|
||||
devices:
|
||||
- /dev/dri:/dev/dri
|
||||
volumes:
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: "3.8"
|
||||
|
||||
# Configurations for hardware-accelerated transcoding
|
||||
|
||||
# If using Unraid or another platform that doesn't allow multiple Compose files,
|
||||
@@ -12,6 +10,9 @@ services:
|
||||
cpu: {}
|
||||
|
||||
nvenc:
|
||||
runtime: nvidia
|
||||
environment:
|
||||
- DISPLAY:$DISPLAY
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
@@ -22,6 +23,8 @@ services:
|
||||
- gpu
|
||||
- compute
|
||||
- video
|
||||
- display
|
||||
- graphics
|
||||
|
||||
quicksync:
|
||||
devices:
|
||||
|
||||
6
e2e/package-lock.json
generated
6
e2e/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "1.105.0",
|
||||
"version": "1.105.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich-e2e",
|
||||
"version": "1.105.0",
|
||||
"version": "1.105.1",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"devDependencies": {
|
||||
"@immich/cli": "file:../cli",
|
||||
@@ -81,7 +81,7 @@
|
||||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.105.0",
|
||||
"version": "1.105.1",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "1.105.0",
|
||||
"version": "1.105.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
ARG DEVICE=cpu
|
||||
|
||||
FROM python:3.11-bookworm@sha256:9df8be5280f00f39931fc2168309e82ddf252a7f30cda6593b7990fb0f5f59f9 as builder-cpu
|
||||
FROM python:3.11-bookworm@sha256:96de1ea4821d73fd2c1853d1fdc3cf794ccfe2fae4c3f08579e846de51760a61 as builder-cpu
|
||||
|
||||
FROM openvino/ubuntu22_runtime:2023.3.0@sha256:176646df619032ea6c10faf842867119c393e7497b7f88b5e307e932a0fd5aa8 as builder-openvino
|
||||
USER root
|
||||
|
||||
12
machine-learning/poetry.lock
generated
12
machine-learning/poetry.lock
generated
@@ -738,13 +738,13 @@ dotenv = ["python-dotenv"]
|
||||
|
||||
[[package]]
|
||||
name = "flask-cors"
|
||||
version = "4.0.0"
|
||||
version = "4.0.1"
|
||||
description = "A Flask extension adding a decorator for CORS support"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "Flask-Cors-4.0.0.tar.gz", hash = "sha256:f268522fcb2f73e2ecdde1ef45e2fd5c71cc48fe03cffb4b441c6d1b40684eb0"},
|
||||
{file = "Flask_Cors-4.0.0-py2.py3-none-any.whl", hash = "sha256:bc3492bfd6368d27cfe79c7821df5a8a319e1a6d5eab277a3794be19bdc51783"},
|
||||
{file = "Flask_Cors-4.0.1-py2.py3-none-any.whl", hash = "sha256:f2a704e4458665580c074b714c4627dd5a306b333deb9074d0b1794dfa2fb677"},
|
||||
{file = "flask_cors-4.0.1.tar.gz", hash = "sha256:eeb69b342142fdbf4766ad99357a7f3876a2ceb77689dc10ff912aac06c389e4"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1374,13 +1374,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.3"
|
||||
version = "3.1.4"
|
||||
description = "A very fast and expressive template engine."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"},
|
||||
{file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"},
|
||||
{file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
|
||||
{file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "machine-learning"
|
||||
version = "1.105.0"
|
||||
version = "1.105.1"
|
||||
description = ""
|
||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||
readme = "README.md"
|
||||
|
||||
@@ -10,16 +10,16 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.925.0)
|
||||
aws-sdk-core (3.194.1)
|
||||
aws-partitions (1.929.0)
|
||||
aws-sdk-core (3.196.1)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.8)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.80.0)
|
||||
aws-sdk-kms (1.81.0)
|
||||
aws-sdk-core (~> 3, >= 3.193.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.149.1)
|
||||
aws-sdk-s3 (1.151.0)
|
||||
aws-sdk-core (~> 3, >= 3.194.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.8)
|
||||
@@ -155,7 +155,7 @@ GEM
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.0)
|
||||
multipart-post (2.4.1)
|
||||
nanaimo (0.3.0)
|
||||
naturally (2.2.1)
|
||||
nkf (0.2.0)
|
||||
|
||||
@@ -81,7 +81,7 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def kotlin_version = '1.9.23'
|
||||
def kotlin_version = '1.9.24'
|
||||
def kotlin_coroutines_version = '1.8.0'
|
||||
def work_version = '2.9.0'
|
||||
def concurrent_version = '1.1.0'
|
||||
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 139,
|
||||
"android.injected.version.name" => "1.105.0",
|
||||
"android.injected.version.code" => 140,
|
||||
"android.injected.version.name" => "1.105.1",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
@@ -5,17 +5,17 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000344">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000374">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="52.933903">
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="84.292464">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="31.026779">
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="33.336934">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ pluginManagement {
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "7.4.2" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.9.23" apply false
|
||||
id "org.jetbrains.kotlin.kapt" version "1.9.23" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.9.24" apply false
|
||||
id "org.jetbrains.kotlin.kapt" version "1.9.24" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
||||
@@ -391,6 +391,7 @@
|
||||
"setting_image_viewer_original_title": "Original laden",
|
||||
"setting_image_viewer_preview_subtitle": "Aktivieren, um ein Bild mit mittlerer Auflösung zu laden. Deaktivieren, um entweder das Original direkt zu laden oder nur die Miniaturansicht zu verwenden.",
|
||||
"setting_image_viewer_preview_title": "Vorschaubild laden",
|
||||
"setting_image_viewer_title": "Bilder",
|
||||
"setting_languages_apply": "Anwenden",
|
||||
"setting_languages_title": "Sprachen",
|
||||
"setting_notifications_notify_failures_grace_period": "Benachrichtigung über Fehler bei der Hintergrundsicherung: {}",
|
||||
@@ -406,6 +407,9 @@
|
||||
"setting_notifications_total_progress_subtitle": "Gesamter Upload-Fortschritt (abgeschlossen/Anzahl Elemente)",
|
||||
"setting_notifications_total_progress_title": "Zeige Gesamtfortschritt bei der Hintergrundsicherung",
|
||||
"setting_pages_app_bar_settings": "Einstellungen",
|
||||
"setting_video_viewer_looping_subtitle": "Aktivieren, damit sich ein Video in der Detailansicht automatisch wiederholt.",
|
||||
"setting_video_viewer_looping_title": "Wiederholen",
|
||||
"setting_video_viewer_title": "Videos",
|
||||
"settings_require_restart": "Bitte starte Immich neu, um diese Einstellung anzuwenden.",
|
||||
"share_add": "Hinzufügen",
|
||||
"share_add_photos": "Fotos hinzufügen",
|
||||
|
||||
@@ -391,6 +391,7 @@
|
||||
"setting_image_viewer_original_title": "Load original image",
|
||||
"setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.",
|
||||
"setting_image_viewer_preview_title": "Load preview image",
|
||||
"setting_image_viewer_title": "Images",
|
||||
"setting_languages_apply": "Apply",
|
||||
"setting_languages_title": "Languages",
|
||||
"setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
|
||||
@@ -406,6 +407,9 @@
|
||||
"setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)",
|
||||
"setting_notifications_total_progress_title": "Show background backup total progress",
|
||||
"setting_pages_app_bar_settings": "Settings",
|
||||
"setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.",
|
||||
"setting_video_viewer_looping_title": "Looping",
|
||||
"setting_video_viewer_title": "Videos",
|
||||
"settings_require_restart": "Please restart Immich to apply this setting",
|
||||
"share_add": "Add",
|
||||
"share_add_photos": "Add photos",
|
||||
|
||||
@@ -10,16 +10,16 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.925.0)
|
||||
aws-sdk-core (3.194.1)
|
||||
aws-partitions (1.929.0)
|
||||
aws-sdk-core (3.196.1)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.8)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.80.0)
|
||||
aws-sdk-kms (1.81.0)
|
||||
aws-sdk-core (~> 3, >= 3.193.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.149.1)
|
||||
aws-sdk-s3 (1.151.0)
|
||||
aws-sdk-core (~> 3, >= 3.194.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.8)
|
||||
@@ -155,7 +155,7 @@ GEM
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.0)
|
||||
multipart-post (2.4.1)
|
||||
nanaimo (0.3.0)
|
||||
naturally (2.2.1)
|
||||
nkf (0.2.0)
|
||||
|
||||
@@ -383,7 +383,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 156;
|
||||
CURRENT_PROJECT_VERSION = 157;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -525,7 +525,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 156;
|
||||
CURRENT_PROJECT_VERSION = 157;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -553,7 +553,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 156;
|
||||
CURRENT_PROJECT_VERSION = 157;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
||||
@@ -58,11 +58,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.104.0</string>
|
||||
<string>1.105.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>156</string>
|
||||
<string>157</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true />
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
@@ -19,7 +19,7 @@ platform :ios do
|
||||
desc "iOS Beta"
|
||||
lane :beta do
|
||||
increment_version_number(
|
||||
version_number: "1.105.0"
|
||||
version_number: "1.105.1"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
@@ -5,32 +5,32 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000406">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.020864">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="37.890919">
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.917777">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="7.615129">
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="5.283943">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.535245">
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.944748">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="213.966277">
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="215.971639">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="80.742201">
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="76.674601">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -182,6 +182,7 @@ enum StoreKey<T> {
|
||||
advancedTroubleshooting<bool>(114, type: bool),
|
||||
logLevel<int>(115, type: int),
|
||||
preferRemoteImage<bool>(116, type: bool),
|
||||
loopVideo<bool>(117, type: bool),
|
||||
// map related settings
|
||||
mapShowFavoriteOnly<bool>(118, type: bool),
|
||||
mapRelativeDate<int>(119, type: int),
|
||||
|
||||
@@ -54,7 +54,7 @@ class AlbumPreviewPage extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
onPressed: () => context.maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -191,7 +191,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
onPressed: () => context.maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
title: const Text(
|
||||
|
||||
@@ -7,16 +7,16 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.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/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
||||
import 'package:immich_mobile/widgets/backup/current_backup_asset_info_box.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/backup/backup_info_card.dart';
|
||||
import 'package:immich_mobile/widgets/backup/current_backup_asset_info_box.dart';
|
||||
|
||||
@RoutePage()
|
||||
class BackupControllerPage extends HookConsumerWidget {
|
||||
@@ -260,7 +260,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
ref.watch(websocketProvider.notifier).listenUploadEvent();
|
||||
context.popRoute(true);
|
||||
context.maybePop(true);
|
||||
},
|
||||
splashRadius: 24,
|
||||
icon: const Icon(
|
||||
|
||||
@@ -13,7 +13,7 @@ class BackupOptionsPage extends StatelessWidget {
|
||||
elevation: 0,
|
||||
title: const Text("backup_options_page_title").tr(),
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(true),
|
||||
onPressed: () => context.maybePop(true),
|
||||
splashRadius: 24,
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios_rounded,
|
||||
|
||||
@@ -23,7 +23,7 @@ class FailedBackupStatusPage extends HookConsumerWidget {
|
||||
),
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
context.popRoute(true);
|
||||
context.maybePop(true);
|
||||
},
|
||||
splashRadius: 24,
|
||||
icon: const Icon(
|
||||
|
||||
@@ -26,7 +26,7 @@ class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget {
|
||||
final sharedUsersList = useState<Set<User>>({});
|
||||
|
||||
addNewUsersHandler() {
|
||||
context.popRoute(sharedUsersList.value.map((e) => e.id).toList());
|
||||
context.maybePop(sharedUsersList.value.map((e) => e.id).toList());
|
||||
}
|
||||
|
||||
buildTileIcon(User user) {
|
||||
@@ -127,7 +127,7 @@ class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget {
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
onPressed: () {
|
||||
context.popRoute(null);
|
||||
context.maybePop(null);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
|
||||
@@ -184,7 +184,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
||||
onPressed: () => context.popRoute(null),
|
||||
onPressed: () => context.maybePop(null),
|
||||
),
|
||||
centerTitle: true,
|
||||
title: Text("translated_text_options".tr()),
|
||||
|
||||
@@ -36,7 +36,7 @@ class AlbumSharedUserSelectionPage extends HookConsumerWidget {
|
||||
await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||
// ref.watch(assetSelectionProvider.notifier).removeAll();
|
||||
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
||||
context.popRoute(true);
|
||||
context.maybePop(true);
|
||||
context
|
||||
.navigateTo(const TabControllerRoute(children: [SharingRoute()]));
|
||||
}
|
||||
@@ -152,7 +152,7 @@ class AlbumSharedUserSelectionPage extends HookConsumerWidget {
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
onPressed: () async {
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
|
||||
@@ -105,7 +105,7 @@ class AppLogPage extends HookConsumerWidget {
|
||||
],
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios_new_rounded,
|
||||
|
||||
@@ -3,16 +3,16 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/album_title.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/album/album_action_outlined_button.dart';
|
||||
import 'package:immich_mobile/widgets/album/album_title_text_field.dart';
|
||||
import 'package:immich_mobile/widgets/album/shared_album_thumbnail_image.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||
|
||||
@RoutePage()
|
||||
// ignore: must_be_immutable
|
||||
@@ -216,7 +216,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
selectedAssets.value = {};
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
},
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
),
|
||||
|
||||
@@ -2,32 +2,33 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/image/immich_remote_image_provider.dart';
|
||||
import 'package:immich_mobile/pages/common/video_viewer.page.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/providers/image/immich_remote_image_provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/advanced_bottom_sheet.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/bottom_gallery_bar.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/exif_sheet/exif_bottom_sheet.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/gallery_app_bar.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/pages/common/video_viewer.page.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_image.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
|
||||
import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart';
|
||||
import 'package:immich_mobile/widgets/photo_view/src/photo_view_computed_scale.dart';
|
||||
import 'package:immich_mobile/widgets/photo_view/src/photo_view_scale_state.dart';
|
||||
import 'package:immich_mobile/widgets/photo_view/src/utils/photo_view_hero_attributes.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:openapi/api.dart' show ThumbnailFormat;
|
||||
|
||||
@@ -59,6 +60,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
final settings = ref.watch(appSettingsServiceProvider);
|
||||
final isLoadPreview = useState(AppSettingsEnum.loadPreview.defaultValue);
|
||||
final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue);
|
||||
final shouldLoopVideo = useState(AppSettingsEnum.loopVideo.defaultValue);
|
||||
final isZoomed = useState(false);
|
||||
final isPlayingVideo = useState(false);
|
||||
final localPosition = useState<Offset?>(null);
|
||||
@@ -101,6 +103,8 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
settings.getSetting<bool>(AppSettingsEnum.loadPreview);
|
||||
isLoadOriginal.value =
|
||||
settings.getSetting<bool>(AppSettingsEnum.loadOriginal);
|
||||
shouldLoopVideo.value =
|
||||
settings.getSetting<bool>(AppSettingsEnum.loopVideo);
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
@@ -174,7 +178,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
|
||||
final ratio = d.dy / max(d.dx.abs(), 1);
|
||||
if (d.dy > sensitivity && ratio > ratioThreshold) {
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
} else if (d.dy < -sensitivity && ratio < -ratioThreshold) {
|
||||
showInfo();
|
||||
}
|
||||
@@ -261,12 +265,9 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvoked: (_) {
|
||||
// Change immersive mode back to normal "edgeToEdge" mode
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
context.pop();
|
||||
},
|
||||
// Change immersive mode back to normal "edgeToEdge" mode
|
||||
onPopInvoked: (_) =>
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
@@ -370,6 +371,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
key: ValueKey(a),
|
||||
asset: a,
|
||||
isMotionVideo: a.livePhotoVideoId != null,
|
||||
loopVideo: shouldLoopVideo.value,
|
||||
placeholder: Image(
|
||||
image: provider,
|
||||
fit: BoxFit.contain,
|
||||
|
||||
@@ -5,8 +5,8 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/widgets/settings/advanced_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/image_viewer_quality_setting.dart';
|
||||
import 'package:immich_mobile/widgets/settings/language_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/notification_setting.dart';
|
||||
import 'package:immich_mobile/widgets/settings/preference_settings/preference_setting.dart';
|
||||
@@ -33,7 +33,7 @@ enum SettingSection {
|
||||
SettingSection.preferences => const PreferenceSetting(),
|
||||
SettingSection.backup => const BackupSettings(),
|
||||
SettingSection.timeline => const AssetListSettings(),
|
||||
SettingSection.viewer => const ImageViewerQualitySetting(),
|
||||
SettingSection.viewer => const AssetViewerSettings(),
|
||||
SettingSection.advanced => const AdvancedSettings(),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/video_player_controller_provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/video_player.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
@RoutePage()
|
||||
// ignore: must_be_immutable
|
||||
class VideoViewerPage extends HookConsumerWidget {
|
||||
final Asset asset;
|
||||
final bool isMotionVideo;
|
||||
@@ -20,6 +17,7 @@ class VideoViewerPage extends HookConsumerWidget {
|
||||
final Duration hideControlsTimer;
|
||||
final bool showControls;
|
||||
final bool showDownloadingIndicator;
|
||||
final bool loopVideo;
|
||||
|
||||
const VideoViewerPage({
|
||||
super.key,
|
||||
@@ -29,6 +27,7 @@ class VideoViewerPage extends HookConsumerWidget {
|
||||
this.showControls = true,
|
||||
this.hideControlsTimer = const Duration(seconds: 5),
|
||||
this.showDownloadingIndicator = true,
|
||||
this.loopVideo = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -76,7 +75,9 @@ class VideoViewerPage extends HookConsumerWidget {
|
||||
// Also sets the error if there is an error in the playback
|
||||
void updateVideoPlayback() {
|
||||
final videoPlayback = VideoPlaybackValue.fromController(controller);
|
||||
ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback;
|
||||
if (!loopVideo) {
|
||||
ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback;
|
||||
}
|
||||
final state = videoPlayback.state;
|
||||
|
||||
// Enable the WakeLock while the video is playing
|
||||
@@ -156,6 +157,7 @@ class VideoViewerPage extends HookConsumerWidget {
|
||||
hideControlsTimer: hideControlsTimer,
|
||||
showControls: showControls,
|
||||
showDownloadingIndicator: showDownloadingIndicator,
|
||||
loopVideo: loopVideo,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -17,7 +17,7 @@ class ArchivePage extends HookConsumerWidget {
|
||||
final count = archivedAssets.value?.totalAssets.toString() ?? "?";
|
||||
return AppBar(
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
onPressed: () => context.maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
centerTitle: true,
|
||||
|
||||
@@ -15,7 +15,7 @@ class FavoritesPage extends HookConsumerWidget {
|
||||
AppBar buildAppBar() {
|
||||
return AppBar(
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
onPressed: () => context.maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
centerTitle: true,
|
||||
|
||||
@@ -143,7 +143,7 @@ class TrashPage extends HookConsumerWidget {
|
||||
return AppBar(
|
||||
leading: IconButton(
|
||||
onPressed: !selectionEnabledHook.value
|
||||
? () => context.popRoute()
|
||||
? () => context.maybePop()
|
||||
: () {
|
||||
selectionEnabledHook.value = false;
|
||||
selection.value = {};
|
||||
|
||||
@@ -176,7 +176,7 @@ class PermissionOnboardingPage extends HookConsumerWidget {
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('permission_onboarding_back').tr(),
|
||||
onPressed: () => context.popRoute(),
|
||||
onPressed: () => context.maybePop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -3,14 +3,14 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/models/memories/memory.model.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_image.dart';
|
||||
import 'package:immich_mobile/widgets/memories/memory_bottom_info.dart';
|
||||
import 'package:immich_mobile/widgets/memories/memory_card.dart';
|
||||
import 'package:immich_mobile/widgets/memories/memory_epilogue.dart';
|
||||
import 'package:immich_mobile/widgets/memories/memory_progress_indicator.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_image.dart';
|
||||
|
||||
@RoutePage()
|
||||
class MemoryPage extends HookConsumerWidget {
|
||||
@@ -153,7 +153,7 @@ class MemoryPage extends HookConsumerWidget {
|
||||
final offset = notification.metrics.pixels;
|
||||
if (isEpiloguePage &&
|
||||
(offset > notification.metrics.maxScrollExtent + 150)) {
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -256,7 +256,7 @@ class MemoryPage extends HookConsumerWidget {
|
||||
// auto_route doesn't invoke pop scope, so
|
||||
// turn off full screen mode here
|
||||
// https://github.com/Milad-Akarie/auto_route_library/issues/1799
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.edgeToEdge,
|
||||
);
|
||||
|
||||
@@ -18,7 +18,7 @@ class AllMotionPhotosPage extends HookConsumerWidget {
|
||||
appBar: AppBar(
|
||||
title: const Text('motion_photos_page_title').tr(),
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
onPressed: () => context.maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -21,7 +21,7 @@ class AllPeoplePage extends HookConsumerWidget {
|
||||
'all_people_page_title',
|
||||
).tr(),
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
onPressed: () => context.maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -22,7 +22,7 @@ class AllPlacesPage extends HookConsumerWidget {
|
||||
'curated_location_page_title',
|
||||
).tr(),
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
onPressed: () => context.maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -15,7 +15,7 @@ class AllVideosPage extends HookConsumerWidget {
|
||||
appBar: AppBar(
|
||||
title: const Text('all_videos_page_title').tr(),
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
onPressed: () => context.maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -41,7 +41,7 @@ class MapLocationPickerPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
void onClose([LatLng? selected]) {
|
||||
context.popRoute(selected);
|
||||
context.maybePop(selected);
|
||||
}
|
||||
|
||||
Future<void> getCurrentLocation() async {
|
||||
|
||||
@@ -102,7 +102,7 @@ class PersonResultPage extends HookConsumerWidget {
|
||||
appBar: AppBar(
|
||||
title: Text(name.value),
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
onPressed: () => context.maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
actions: [
|
||||
|
||||
@@ -18,7 +18,7 @@ class RecentlyAddedPage extends HookConsumerWidget {
|
||||
appBar: AppBar(
|
||||
title: const Text('recently_added_page_title').tr(),
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
onPressed: () => context.maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -4,9 +4,11 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/providers/search/paginated_search.provider.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
|
||||
import 'package:immich_mobile/widgets/search/search_filter/camera_picker.dart';
|
||||
import 'package:immich_mobile/widgets/search/search_filter/display_option_picker.dart';
|
||||
import 'package:immich_mobile/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart';
|
||||
@@ -15,8 +17,6 @@ import 'package:immich_mobile/widgets/search/search_filter/media_type_picker.dar
|
||||
import 'package:immich_mobile/widgets/search/search_filter/people_picker.dart';
|
||||
import 'package:immich_mobile/widgets/search/search_filter/search_filter_chip.dart';
|
||||
import 'package:immich_mobile/widgets/search/search_filter/search_filter_utils.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
@RoutePage()
|
||||
@@ -480,9 +480,7 @@ class SearchInputPage extends HookConsumerWidget {
|
||||
],
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
||||
onPressed: () {
|
||||
context.router.pop();
|
||||
},
|
||||
onPressed: () => context.router.maybePop(),
|
||||
),
|
||||
title: TextField(
|
||||
controller: textSearchController,
|
||||
|
||||
@@ -328,7 +328,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
alignment: Alignment.bottomRight,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
},
|
||||
child: const Text(
|
||||
"share_done",
|
||||
@@ -431,7 +431,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
changeExpiry: changeExpiry,
|
||||
);
|
||||
ref.invalidate(sharedLinksStateProvider);
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
|
||||
@@ -1,35 +1,32 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/models/memories/memory.model.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/pages/common/activities.page.dart';
|
||||
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
|
||||
import 'package:immich_mobile/pages/common/album_options.page.dart';
|
||||
import 'package:immich_mobile/pages/common/album_viewer.page.dart';
|
||||
import 'package:immich_mobile/pages/common/album_asset_selection.page.dart';
|
||||
import 'package:immich_mobile/pages/common/create_album.page.dart';
|
||||
import 'package:immich_mobile/models/shared_link/shared_link.model.dart';
|
||||
import 'package:immich_mobile/pages/common/album_additional_shared_user_selection.page.dart';
|
||||
import 'package:immich_mobile/pages/common/album_shared_user_selection.page.dart';
|
||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/routing/auth_guard.dart';
|
||||
import 'package:immich_mobile/routing/custom_transition_builders.dart';
|
||||
import 'package:immich_mobile/routing/duplicate_guard.dart';
|
||||
import 'package:immich_mobile/routing/backup_permission_guard.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/logger_message.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/pages/common/app_log_detail.page.dart';
|
||||
import 'package:immich_mobile/pages/common/app_log.page.dart';
|
||||
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
|
||||
import 'package:immich_mobile/models/memories/memory.model.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/models/shared_link/shared_link.model.dart';
|
||||
import 'package:immich_mobile/pages/backup/album_preview.page.dart';
|
||||
import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart';
|
||||
import 'package:immich_mobile/pages/backup/backup_controller.page.dart';
|
||||
import 'package:immich_mobile/pages/backup/backup_options.page.dart';
|
||||
import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
|
||||
import 'package:immich_mobile/pages/common/activities.page.dart';
|
||||
import 'package:immich_mobile/pages/common/album_additional_shared_user_selection.page.dart';
|
||||
import 'package:immich_mobile/pages/common/album_asset_selection.page.dart';
|
||||
import 'package:immich_mobile/pages/common/album_options.page.dart';
|
||||
import 'package:immich_mobile/pages/common/album_shared_user_selection.page.dart';
|
||||
import 'package:immich_mobile/pages/common/album_viewer.page.dart';
|
||||
import 'package:immich_mobile/pages/common/app_log.page.dart';
|
||||
import 'package:immich_mobile/pages/common/app_log_detail.page.dart';
|
||||
import 'package:immich_mobile/pages/common/create_album.page.dart';
|
||||
import 'package:immich_mobile/pages/common/gallery_viewer.page.dart';
|
||||
import 'package:immich_mobile/pages/common/settings.page.dart';
|
||||
import 'package:immich_mobile/pages/common/splash_screen.page.dart';
|
||||
import 'package:immich_mobile/pages/common/tab_controller.page.dart';
|
||||
import 'package:immich_mobile/pages/library/archive.page.dart';
|
||||
import 'package:immich_mobile/pages/library/favorite.page.dart';
|
||||
import 'package:immich_mobile/pages/library/library.page.dart';
|
||||
@@ -43,21 +40,23 @@ import 'package:immich_mobile/pages/search/all_motion_videos.page.dart';
|
||||
import 'package:immich_mobile/pages/search/all_people.page.dart';
|
||||
import 'package:immich_mobile/pages/search/all_places.page.dart';
|
||||
import 'package:immich_mobile/pages/search/all_videos.page.dart';
|
||||
import 'package:immich_mobile/pages/search/map/map_location_picker.page.dart';
|
||||
import 'package:immich_mobile/pages/search/map/map.page.dart';
|
||||
import 'package:immich_mobile/pages/search/map/map_location_picker.page.dart';
|
||||
import 'package:immich_mobile/pages/search/person_result.page.dart';
|
||||
import 'package:immich_mobile/pages/search/recently_added.page.dart';
|
||||
import 'package:immich_mobile/pages/search/search_input.page.dart';
|
||||
import 'package:immich_mobile/pages/search/search.page.dart';
|
||||
import 'package:immich_mobile/pages/common/settings.page.dart';
|
||||
import 'package:immich_mobile/pages/sharing/partner/partner_detail.page.dart';
|
||||
import 'package:immich_mobile/pages/search/search_input.page.dart';
|
||||
import 'package:immich_mobile/pages/sharing/partner/partner.page.dart';
|
||||
import 'package:immich_mobile/pages/sharing/shared_link/shared_link_edit.page.dart';
|
||||
import 'package:immich_mobile/pages/sharing/partner/partner_detail.page.dart';
|
||||
import 'package:immich_mobile/pages/sharing/shared_link/shared_link.page.dart';
|
||||
import 'package:immich_mobile/pages/sharing/shared_link/shared_link_edit.page.dart';
|
||||
import 'package:immich_mobile/pages/sharing/sharing.page.dart';
|
||||
import 'package:immich_mobile/pages/common/splash_screen.page.dart';
|
||||
import 'package:immich_mobile/pages/common/tab_controller.page.dart';
|
||||
import 'package:immich_mobile/pages/common/video_viewer.page.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/routing/auth_guard.dart';
|
||||
import 'package:immich_mobile/routing/backup_permission_guard.dart';
|
||||
import 'package:immich_mobile/routing/custom_transition_builders.dart';
|
||||
import 'package:immich_mobile/routing/duplicate_guard.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
@@ -120,10 +119,6 @@ class AppRouter extends _$AppRouter {
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
transitionsBuilder: CustomTransitionsBuilders.zoomedPage,
|
||||
),
|
||||
AutoRoute(
|
||||
page: VideoViewerRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
),
|
||||
AutoRoute(
|
||||
page: BackupControllerRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard, _backupPermissionGuard],
|
||||
|
||||
@@ -21,6 +21,29 @@ abstract class _$AppRouter extends RootStackRouter {
|
||||
child: const ActivitiesPage(),
|
||||
);
|
||||
},
|
||||
AlbumAdditionalSharedUserSelectionRoute.name: (routeData) {
|
||||
final args =
|
||||
routeData.argsAs<AlbumAdditionalSharedUserSelectionRouteArgs>();
|
||||
return AutoRoutePage<List<String>?>(
|
||||
routeData: routeData,
|
||||
child: AlbumAdditionalSharedUserSelectionPage(
|
||||
key: args.key,
|
||||
album: args.album,
|
||||
),
|
||||
);
|
||||
},
|
||||
AlbumAssetSelectionRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<AlbumAssetSelectionRouteArgs>();
|
||||
return AutoRoutePage<AssetSelectionPageResult?>(
|
||||
routeData: routeData,
|
||||
child: AlbumAssetSelectionPage(
|
||||
key: args.key,
|
||||
existingAssets: args.existingAssets,
|
||||
canDeselect: args.canDeselect,
|
||||
query: args.query,
|
||||
),
|
||||
);
|
||||
},
|
||||
AlbumOptionsRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<AlbumOptionsRouteArgs>();
|
||||
return AutoRoutePage<dynamic>(
|
||||
@@ -41,6 +64,16 @@ abstract class _$AppRouter extends RootStackRouter {
|
||||
),
|
||||
);
|
||||
},
|
||||
AlbumSharedUserSelectionRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<AlbumSharedUserSelectionRouteArgs>();
|
||||
return AutoRoutePage<List<String>>(
|
||||
routeData: routeData,
|
||||
child: AlbumSharedUserSelectionPage(
|
||||
key: args.key,
|
||||
assets: args.assets,
|
||||
),
|
||||
);
|
||||
},
|
||||
AlbumViewerRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<AlbumViewerRouteArgs>();
|
||||
return AutoRoutePage<dynamic>(
|
||||
@@ -97,18 +130,6 @@ abstract class _$AppRouter extends RootStackRouter {
|
||||
child: const ArchivePage(),
|
||||
);
|
||||
},
|
||||
AlbumAssetSelectionRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<AssetSelectionRouteArgs>();
|
||||
return AutoRoutePage<AssetSelectionPageResult?>(
|
||||
routeData: routeData,
|
||||
child: AlbumAssetSelectionPage(
|
||||
key: args.key,
|
||||
existingAssets: args.existingAssets,
|
||||
canDeselect: args.canDeselect,
|
||||
query: args.query,
|
||||
),
|
||||
);
|
||||
},
|
||||
BackupAlbumSelectionRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
@@ -170,12 +191,6 @@ abstract class _$AppRouter extends RootStackRouter {
|
||||
),
|
||||
);
|
||||
},
|
||||
PhotosRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const PhotosPage(),
|
||||
);
|
||||
},
|
||||
LibraryRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
@@ -249,6 +264,12 @@ abstract class _$AppRouter extends RootStackRouter {
|
||||
),
|
||||
);
|
||||
},
|
||||
PhotosRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const PhotosPage(),
|
||||
);
|
||||
},
|
||||
RecentlyAddedRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
@@ -272,26 +293,6 @@ abstract class _$AppRouter extends RootStackRouter {
|
||||
child: const SearchPage(),
|
||||
);
|
||||
},
|
||||
AlbumAdditionalSharedUserSelectionRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<SelectAdditionalUserForSharingRouteArgs>();
|
||||
return AutoRoutePage<List<String>?>(
|
||||
routeData: routeData,
|
||||
child: AlbumAdditionalSharedUserSelectionPage(
|
||||
key: args.key,
|
||||
album: args.album,
|
||||
),
|
||||
);
|
||||
},
|
||||
AlbumSharedUserSelectionRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<SelectUserForSharingRouteArgs>();
|
||||
return AutoRoutePage<List<String>>(
|
||||
routeData: routeData,
|
||||
child: AlbumSharedUserSelectionPage(
|
||||
key: args.key,
|
||||
assets: args.assets,
|
||||
),
|
||||
);
|
||||
},
|
||||
SettingsRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
@@ -351,21 +352,6 @@ abstract class _$AppRouter extends RootStackRouter {
|
||||
child: const TrashPage(),
|
||||
);
|
||||
},
|
||||
VideoViewerRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<VideoViewerRouteArgs>();
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: VideoViewerPage(
|
||||
key: args.key,
|
||||
asset: args.asset,
|
||||
isMotionVideo: args.isMotionVideo,
|
||||
placeholder: args.placeholder,
|
||||
showControls: args.showControls,
|
||||
hideControlsTimer: args.hideControlsTimer,
|
||||
showDownloadingIndicator: args.showDownloadingIndicator,
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -383,6 +369,94 @@ class ActivitiesRoute extends PageRouteInfo<void> {
|
||||
static const PageInfo<void> page = PageInfo<void>(name);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AlbumAdditionalSharedUserSelectionPage]
|
||||
class AlbumAdditionalSharedUserSelectionRoute
|
||||
extends PageRouteInfo<AlbumAdditionalSharedUserSelectionRouteArgs> {
|
||||
AlbumAdditionalSharedUserSelectionRoute({
|
||||
Key? key,
|
||||
required Album album,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
AlbumAdditionalSharedUserSelectionRoute.name,
|
||||
args: AlbumAdditionalSharedUserSelectionRouteArgs(
|
||||
key: key,
|
||||
album: album,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'AlbumAdditionalSharedUserSelectionRoute';
|
||||
|
||||
static const PageInfo<AlbumAdditionalSharedUserSelectionRouteArgs> page =
|
||||
PageInfo<AlbumAdditionalSharedUserSelectionRouteArgs>(name);
|
||||
}
|
||||
|
||||
class AlbumAdditionalSharedUserSelectionRouteArgs {
|
||||
const AlbumAdditionalSharedUserSelectionRouteArgs({
|
||||
this.key,
|
||||
required this.album,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final Album album;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AlbumAdditionalSharedUserSelectionRouteArgs{key: $key, album: $album}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AlbumAssetSelectionPage]
|
||||
class AlbumAssetSelectionRoute
|
||||
extends PageRouteInfo<AlbumAssetSelectionRouteArgs> {
|
||||
AlbumAssetSelectionRoute({
|
||||
Key? key,
|
||||
required Set<Asset> existingAssets,
|
||||
bool canDeselect = false,
|
||||
required QueryBuilder<Asset, Asset, QAfterSortBy>? query,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
AlbumAssetSelectionRoute.name,
|
||||
args: AlbumAssetSelectionRouteArgs(
|
||||
key: key,
|
||||
existingAssets: existingAssets,
|
||||
canDeselect: canDeselect,
|
||||
query: query,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'AlbumAssetSelectionRoute';
|
||||
|
||||
static const PageInfo<AlbumAssetSelectionRouteArgs> page =
|
||||
PageInfo<AlbumAssetSelectionRouteArgs>(name);
|
||||
}
|
||||
|
||||
class AlbumAssetSelectionRouteArgs {
|
||||
const AlbumAssetSelectionRouteArgs({
|
||||
this.key,
|
||||
required this.existingAssets,
|
||||
this.canDeselect = false,
|
||||
required this.query,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final Set<Asset> existingAssets;
|
||||
|
||||
final bool canDeselect;
|
||||
|
||||
final QueryBuilder<Asset, Asset, QAfterSortBy>? query;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AlbumAssetSelectionRouteArgs{key: $key, existingAssets: $existingAssets, canDeselect: $canDeselect, query: $query}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AlbumOptionsPage]
|
||||
class AlbumOptionsRoute extends PageRouteInfo<AlbumOptionsRouteArgs> {
|
||||
@@ -459,6 +533,45 @@ class AlbumPreviewRouteArgs {
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AlbumSharedUserSelectionPage]
|
||||
class AlbumSharedUserSelectionRoute
|
||||
extends PageRouteInfo<AlbumSharedUserSelectionRouteArgs> {
|
||||
AlbumSharedUserSelectionRoute({
|
||||
Key? key,
|
||||
required Set<Asset> assets,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
AlbumSharedUserSelectionRoute.name,
|
||||
args: AlbumSharedUserSelectionRouteArgs(
|
||||
key: key,
|
||||
assets: assets,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'AlbumSharedUserSelectionRoute';
|
||||
|
||||
static const PageInfo<AlbumSharedUserSelectionRouteArgs> page =
|
||||
PageInfo<AlbumSharedUserSelectionRouteArgs>(name);
|
||||
}
|
||||
|
||||
class AlbumSharedUserSelectionRouteArgs {
|
||||
const AlbumSharedUserSelectionRouteArgs({
|
||||
this.key,
|
||||
required this.assets,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final Set<Asset> assets;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AlbumSharedUserSelectionRouteArgs{key: $key, assets: $assets}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AlbumViewerPage]
|
||||
class AlbumViewerRoute extends PageRouteInfo<AlbumViewerRouteArgs> {
|
||||
@@ -619,54 +732,6 @@ class ArchiveRoute extends PageRouteInfo<void> {
|
||||
static const PageInfo<void> page = PageInfo<void>(name);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AlbumAssetSelectionPage]
|
||||
class AlbumAssetSelectionRoute extends PageRouteInfo<AssetSelectionRouteArgs> {
|
||||
AlbumAssetSelectionRoute({
|
||||
Key? key,
|
||||
required Set<Asset> existingAssets,
|
||||
bool canDeselect = false,
|
||||
required QueryBuilder<Asset, Asset, QAfterSortBy>? query,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
AlbumAssetSelectionRoute.name,
|
||||
args: AssetSelectionRouteArgs(
|
||||
key: key,
|
||||
existingAssets: existingAssets,
|
||||
canDeselect: canDeselect,
|
||||
query: query,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'AlbumAssetSelectionRoute';
|
||||
|
||||
static const PageInfo<AssetSelectionRouteArgs> page =
|
||||
PageInfo<AssetSelectionRouteArgs>(name);
|
||||
}
|
||||
|
||||
class AssetSelectionRouteArgs {
|
||||
const AssetSelectionRouteArgs({
|
||||
this.key,
|
||||
required this.existingAssets,
|
||||
this.canDeselect = false,
|
||||
required this.query,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final Set<Asset> existingAssets;
|
||||
|
||||
final bool canDeselect;
|
||||
|
||||
final QueryBuilder<Asset, Asset, QAfterSortBy>? query;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AssetSelectionRouteArgs{key: $key, existingAssets: $existingAssets, canDeselect: $canDeselect, query: $query}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [BackupAlbumSelectionPage]
|
||||
class BackupAlbumSelectionRoute extends PageRouteInfo<void> {
|
||||
@@ -852,20 +917,6 @@ class GalleryViewerRouteArgs {
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [PhotosPage]
|
||||
class PhotosRoute extends PageRouteInfo<void> {
|
||||
const PhotosRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
PhotosRoute.name,
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'PhotosRoute';
|
||||
|
||||
static const PageInfo<void> page = PageInfo<void>(name);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [LibraryPage]
|
||||
class LibraryRoute extends PageRouteInfo<void> {
|
||||
@@ -1097,6 +1148,20 @@ class PersonResultRouteArgs {
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [PhotosPage]
|
||||
class PhotosRoute extends PageRouteInfo<void> {
|
||||
const PhotosRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
PhotosRoute.name,
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'PhotosRoute';
|
||||
|
||||
static const PageInfo<void> page = PageInfo<void>(name);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [RecentlyAddedPage]
|
||||
class RecentlyAddedRoute extends PageRouteInfo<void> {
|
||||
@@ -1163,84 +1228,6 @@ class SearchRoute extends PageRouteInfo<void> {
|
||||
static const PageInfo<void> page = PageInfo<void>(name);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AlbumAdditionalSharedUserSelectionPage]
|
||||
class AlbumAdditionalSharedUserSelectionRoute
|
||||
extends PageRouteInfo<SelectAdditionalUserForSharingRouteArgs> {
|
||||
AlbumAdditionalSharedUserSelectionRoute({
|
||||
Key? key,
|
||||
required Album album,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
AlbumAdditionalSharedUserSelectionRoute.name,
|
||||
args: SelectAdditionalUserForSharingRouteArgs(
|
||||
key: key,
|
||||
album: album,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'AlbumAdditionalSharedUserSelectionRoute';
|
||||
|
||||
static const PageInfo<SelectAdditionalUserForSharingRouteArgs> page =
|
||||
PageInfo<SelectAdditionalUserForSharingRouteArgs>(name);
|
||||
}
|
||||
|
||||
class SelectAdditionalUserForSharingRouteArgs {
|
||||
const SelectAdditionalUserForSharingRouteArgs({
|
||||
this.key,
|
||||
required this.album,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final Album album;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SelectAdditionalUserForSharingRouteArgs{key: $key, album: $album}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AlbumSharedUserSelectionPage]
|
||||
class AlbumSharedUserSelectionRoute
|
||||
extends PageRouteInfo<SelectUserForSharingRouteArgs> {
|
||||
AlbumSharedUserSelectionRoute({
|
||||
Key? key,
|
||||
required Set<Asset> assets,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
AlbumSharedUserSelectionRoute.name,
|
||||
args: SelectUserForSharingRouteArgs(
|
||||
key: key,
|
||||
assets: assets,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'AlbumSharedUserSelectionRoute';
|
||||
|
||||
static const PageInfo<SelectUserForSharingRouteArgs> page =
|
||||
PageInfo<SelectUserForSharingRouteArgs>(name);
|
||||
}
|
||||
|
||||
class SelectUserForSharingRouteArgs {
|
||||
const SelectUserForSharingRouteArgs({
|
||||
this.key,
|
||||
required this.assets,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final Set<Asset> assets;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SelectUserForSharingRouteArgs{key: $key, assets: $assets}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [SettingsPage]
|
||||
class SettingsRoute extends PageRouteInfo<void> {
|
||||
@@ -1410,66 +1397,3 @@ class TrashRoute extends PageRouteInfo<void> {
|
||||
|
||||
static const PageInfo<void> page = PageInfo<void>(name);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [VideoViewerPage]
|
||||
class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
|
||||
VideoViewerRoute({
|
||||
Key? key,
|
||||
required Asset asset,
|
||||
bool isMotionVideo = false,
|
||||
Widget? placeholder,
|
||||
bool showControls = true,
|
||||
Duration hideControlsTimer = const Duration(seconds: 5),
|
||||
bool showDownloadingIndicator = true,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
VideoViewerRoute.name,
|
||||
args: VideoViewerRouteArgs(
|
||||
key: key,
|
||||
asset: asset,
|
||||
isMotionVideo: isMotionVideo,
|
||||
placeholder: placeholder,
|
||||
showControls: showControls,
|
||||
hideControlsTimer: hideControlsTimer,
|
||||
showDownloadingIndicator: showDownloadingIndicator,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'VideoViewerRoute';
|
||||
|
||||
static const PageInfo<VideoViewerRouteArgs> page =
|
||||
PageInfo<VideoViewerRouteArgs>(name);
|
||||
}
|
||||
|
||||
class VideoViewerRouteArgs {
|
||||
const VideoViewerRouteArgs({
|
||||
this.key,
|
||||
required this.asset,
|
||||
this.isMotionVideo = false,
|
||||
this.placeholder,
|
||||
this.showControls = true,
|
||||
this.hideControlsTimer = const Duration(seconds: 5),
|
||||
this.showDownloadingIndicator = true,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final Asset asset;
|
||||
|
||||
final bool isMotionVideo;
|
||||
|
||||
final Widget? placeholder;
|
||||
|
||||
final bool showControls;
|
||||
|
||||
final Duration hideControlsTimer;
|
||||
|
||||
final bool showDownloadingIndicator;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'VideoViewerRouteArgs{key: $key, asset: $asset, isMotionVideo: $isMotionVideo, placeholder: $placeholder, showControls: $showControls, hideControlsTimer: $hideControlsTimer, showDownloadingIndicator: $showDownloadingIndicator}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ enum AppSettingsEnum<T> {
|
||||
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
|
||||
logLevel<int>(StoreKey.logLevel, null, 5), // Level.INFO = 5
|
||||
preferRemoteImage<bool>(StoreKey.preferRemoteImage, null, false),
|
||||
loopVideo<bool>(StoreKey.loopVideo, "loopVideo", true),
|
||||
mapThemeMode<int>(StoreKey.mapThemeMode, null, 0),
|
||||
mapShowFavoriteOnly<bool>(StoreKey.mapShowFavoriteOnly, null, false),
|
||||
mapIncludeArchived<bool>(StoreKey.mapIncludeArchived, null, false),
|
||||
|
||||
@@ -17,6 +17,7 @@ ChewieController useChewieController({
|
||||
bool allowFullScreen = false,
|
||||
bool allowedScreenSleep = false,
|
||||
bool showControls = true,
|
||||
bool loopVideo = false,
|
||||
Widget? customControls,
|
||||
Widget? placeholder,
|
||||
Duration hideControlsTimer = const Duration(seconds: 1),
|
||||
@@ -36,6 +37,7 @@ ChewieController useChewieController({
|
||||
hideControlsTimer: hideControlsTimer,
|
||||
showControlsOnInitialize: showControlsOnInitialize,
|
||||
showControls: showControls,
|
||||
loopVideo: loopVideo,
|
||||
allowedScreenSleep: allowedScreenSleep,
|
||||
onPlaying: onPlaying,
|
||||
onPaused: onPaused,
|
||||
@@ -53,6 +55,7 @@ class _ChewieControllerHook extends Hook<ChewieController> {
|
||||
final bool allowFullScreen;
|
||||
final bool allowedScreenSleep;
|
||||
final bool showControls;
|
||||
final bool loopVideo;
|
||||
final Widget? customControls;
|
||||
final Widget? placeholder;
|
||||
final Duration hideControlsTimer;
|
||||
@@ -71,6 +74,7 @@ class _ChewieControllerHook extends Hook<ChewieController> {
|
||||
this.allowFullScreen = false,
|
||||
this.allowedScreenSleep = false,
|
||||
this.showControls = true,
|
||||
this.loopVideo = false,
|
||||
this.customControls,
|
||||
this.placeholder,
|
||||
this.hideControlsTimer = const Duration(seconds: 3),
|
||||
@@ -94,6 +98,7 @@ class _ChewieControllerHookState
|
||||
allowFullScreen: hook.allowFullScreen,
|
||||
allowedScreenSleep: hook.allowedScreenSleep,
|
||||
showControls: hook.showControls,
|
||||
looping: hook.loopVideo,
|
||||
customControls: hook.customControls,
|
||||
placeholder: hook.placeholder,
|
||||
hideControlsTimer: hook.hideControlsTimer,
|
||||
|
||||
@@ -275,7 +275,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||
);
|
||||
} else {
|
||||
return IconButton(
|
||||
onPressed: () async => await context.popRoute(),
|
||||
onPressed: () async => await context.maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
splashRadius: 25,
|
||||
);
|
||||
|
||||
@@ -110,7 +110,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
if (isDeleted && isParent) {
|
||||
if (totalAssets == 1) {
|
||||
// Handle only one asset
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
} else {
|
||||
// Go to next page otherwise
|
||||
controller.nextPage(
|
||||
@@ -181,7 +181,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
stackElements.elementAt(stackIndex),
|
||||
);
|
||||
ctx.pop();
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
},
|
||||
title: const Text(
|
||||
"viewer_stack_use_as_main_asset",
|
||||
@@ -208,7 +208,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
childrenToRemove: [asset],
|
||||
);
|
||||
ctx.pop();
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
} else {
|
||||
await ref.read(assetStackServiceProvider).updateStack(
|
||||
asset,
|
||||
@@ -236,7 +236,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
childrenToRemove: stack,
|
||||
);
|
||||
ctx.pop();
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
},
|
||||
title: const Text(
|
||||
"viewer_unstack",
|
||||
@@ -267,7 +267,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
handleArchive() {
|
||||
ref.read(assetProvider.notifier).toggleArchive([asset]);
|
||||
if (isParent) {
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
return;
|
||||
}
|
||||
removeAssetFromStack();
|
||||
|
||||
@@ -161,7 +161,7 @@ class TopControlAppBar extends HookConsumerWidget {
|
||||
Widget buildBackButton() {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.arrow_back_ios_new_rounded,
|
||||
|
||||
@@ -12,6 +12,7 @@ class VideoPlayerViewer extends HookConsumerWidget {
|
||||
final Duration hideControlsTimer;
|
||||
final bool showControls;
|
||||
final bool showDownloadingIndicator;
|
||||
final bool loopVideo;
|
||||
|
||||
const VideoPlayerViewer({
|
||||
super.key,
|
||||
@@ -21,6 +22,7 @@ class VideoPlayerViewer extends HookConsumerWidget {
|
||||
required this.hideControlsTimer,
|
||||
required this.showControls,
|
||||
required this.showDownloadingIndicator,
|
||||
required this.loopVideo,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -36,6 +38,7 @@ class VideoPlayerViewer extends HookConsumerWidget {
|
||||
),
|
||||
showControls: showControls && !isMotionVideo,
|
||||
hideControlsTimer: hideControlsTimer,
|
||||
loopVideo: loopVideo,
|
||||
);
|
||||
|
||||
return Chewie(
|
||||
|
||||
@@ -79,7 +79,7 @@ class _LocationPicker extends HookWidget {
|
||||
).tr(),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => context.popRoute(latlng),
|
||||
onPressed: () => context.maybePop(latlng),
|
||||
child: Text(
|
||||
"action_common_update",
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
|
||||
@@ -50,7 +50,7 @@ class _NonSelectionRow extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
onPressed: () => context.maybePop(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
|
||||
@@ -43,7 +43,7 @@ class MemoryBottomInfo extends StatelessWidget {
|
||||
MaterialButton(
|
||||
minWidth: 0,
|
||||
onPressed: () {
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
scrollToDateNotifierProvider
|
||||
.scrollToDate(memory.assets[0].fileCreatedAt);
|
||||
},
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
||||
import 'video_viewer_settings.dart';
|
||||
|
||||
class AssetViewerSettings extends StatelessWidget {
|
||||
const AssetViewerSettings({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final assetViewerSetting = [
|
||||
const ImageViewerQualitySetting(),
|
||||
const VideoViewerSettings(),
|
||||
];
|
||||
|
||||
return SettingsSubPageScaffold(
|
||||
settings: assetViewerSetting,
|
||||
showDivider: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
|
||||
class ImageViewerQualitySetting extends HookConsumerWidget {
|
||||
const ImageViewerQualitySetting({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isPreview = useAppSettingsState(AppSettingsEnum.loadPreview);
|
||||
final isOriginal = useAppSettingsState(AppSettingsEnum.loadOriginal);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SettingsSubTitle(title: "setting_image_viewer_title".tr()),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
title: Text(
|
||||
'setting_image_viewer_help',
|
||||
style: context.textTheme.bodyMedium,
|
||||
).tr(),
|
||||
),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: isPreview,
|
||||
title: "setting_image_viewer_preview_title".tr(),
|
||||
subtitle: "setting_image_viewer_preview_subtitle".tr(),
|
||||
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||
),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: isOriginal,
|
||||
title: "setting_image_viewer_original_title".tr(),
|
||||
subtitle: "setting_image_viewer_original_subtitle".tr(),
|
||||
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
|
||||
class VideoViewerSettings extends HookConsumerWidget {
|
||||
const VideoViewerSettings({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final useLoopVideo = useAppSettingsState(AppSettingsEnum.loopVideo);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SettingsSubTitle(title: "setting_video_viewer_title".tr()),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: useLoopVideo,
|
||||
title: "setting_video_viewer_looping_title".tr(),
|
||||
subtitle: "setting_video_viewer_looping_subtitle".tr(),
|
||||
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
|
||||
class ImageViewerQualitySetting extends HookWidget {
|
||||
const ImageViewerQualitySetting({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isPreview = useAppSettingsState(AppSettingsEnum.loadPreview);
|
||||
final isOriginal = useAppSettingsState(AppSettingsEnum.loadOriginal);
|
||||
|
||||
final viewerSettings = [
|
||||
ListTile(
|
||||
title: Text(
|
||||
'setting_image_viewer_help',
|
||||
style: context.textTheme.bodyMedium,
|
||||
).tr(),
|
||||
),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: isPreview,
|
||||
title: "setting_image_viewer_preview_title".tr(),
|
||||
subtitle: "setting_image_viewer_preview_subtitle".tr(),
|
||||
),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: isOriginal,
|
||||
title: "setting_image_viewer_original_title".tr(),
|
||||
subtitle: "setting_image_viewer_original_subtitle".tr(),
|
||||
),
|
||||
];
|
||||
|
||||
return SettingsSubPageScaffold(settings: viewerSettings);
|
||||
}
|
||||
}
|
||||
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 1.105.0
|
||||
- API version: 1.105.1
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
## Requirements
|
||||
|
||||
1
mobile/openapi/doc/SystemConfigFFmpegDto.md
generated
1
mobile/openapi/doc/SystemConfigFFmpegDto.md
generated
@@ -9,6 +9,7 @@ import 'package:openapi/api.dart';
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**accel** | [**TranscodeHWAccel**](TranscodeHWAccel.md) | |
|
||||
**accelDecode** | **bool** | |
|
||||
**acceptedAudioCodecs** | [**List<AudioCodec>**](AudioCodec.md) | | [default to const []]
|
||||
**acceptedVideoCodecs** | [**List<VideoCodec>**](VideoCodec.md) | | [default to const []]
|
||||
**bframes** | **int** | |
|
||||
|
||||
@@ -14,6 +14,7 @@ class SystemConfigFFmpegDto {
|
||||
/// Returns a new [SystemConfigFFmpegDto] instance.
|
||||
SystemConfigFFmpegDto({
|
||||
required this.accel,
|
||||
required this.accelDecode,
|
||||
this.acceptedAudioCodecs = const [],
|
||||
this.acceptedVideoCodecs = const [],
|
||||
required this.bframes,
|
||||
@@ -37,6 +38,8 @@ class SystemConfigFFmpegDto {
|
||||
|
||||
TranscodeHWAccel accel;
|
||||
|
||||
bool accelDecode;
|
||||
|
||||
List<AudioCodec> acceptedAudioCodecs;
|
||||
|
||||
List<VideoCodec> acceptedVideoCodecs;
|
||||
@@ -87,6 +90,7 @@ class SystemConfigFFmpegDto {
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SystemConfigFFmpegDto &&
|
||||
other.accel == accel &&
|
||||
other.accelDecode == accelDecode &&
|
||||
_deepEquality.equals(other.acceptedAudioCodecs, acceptedAudioCodecs) &&
|
||||
_deepEquality.equals(other.acceptedVideoCodecs, acceptedVideoCodecs) &&
|
||||
other.bframes == bframes &&
|
||||
@@ -111,6 +115,7 @@ class SystemConfigFFmpegDto {
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(accel.hashCode) +
|
||||
(accelDecode.hashCode) +
|
||||
(acceptedAudioCodecs.hashCode) +
|
||||
(acceptedVideoCodecs.hashCode) +
|
||||
(bframes.hashCode) +
|
||||
@@ -132,11 +137,12 @@ class SystemConfigFFmpegDto {
|
||||
(twoPass.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SystemConfigFFmpegDto[accel=$accel, acceptedAudioCodecs=$acceptedAudioCodecs, acceptedVideoCodecs=$acceptedVideoCodecs, bframes=$bframes, cqMode=$cqMode, crf=$crf, gopSize=$gopSize, maxBitrate=$maxBitrate, npl=$npl, preferredHwDevice=$preferredHwDevice, preset=$preset, refs=$refs, targetAudioCodec=$targetAudioCodec, targetResolution=$targetResolution, targetVideoCodec=$targetVideoCodec, temporalAQ=$temporalAQ, threads=$threads, tonemap=$tonemap, transcode=$transcode, twoPass=$twoPass]';
|
||||
String toString() => 'SystemConfigFFmpegDto[accel=$accel, accelDecode=$accelDecode, acceptedAudioCodecs=$acceptedAudioCodecs, acceptedVideoCodecs=$acceptedVideoCodecs, bframes=$bframes, cqMode=$cqMode, crf=$crf, gopSize=$gopSize, maxBitrate=$maxBitrate, npl=$npl, preferredHwDevice=$preferredHwDevice, preset=$preset, refs=$refs, targetAudioCodec=$targetAudioCodec, targetResolution=$targetResolution, targetVideoCodec=$targetVideoCodec, temporalAQ=$temporalAQ, threads=$threads, tonemap=$tonemap, transcode=$transcode, twoPass=$twoPass]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'accel'] = this.accel;
|
||||
json[r'accelDecode'] = this.accelDecode;
|
||||
json[r'acceptedAudioCodecs'] = this.acceptedAudioCodecs;
|
||||
json[r'acceptedVideoCodecs'] = this.acceptedVideoCodecs;
|
||||
json[r'bframes'] = this.bframes;
|
||||
@@ -168,6 +174,7 @@ class SystemConfigFFmpegDto {
|
||||
|
||||
return SystemConfigFFmpegDto(
|
||||
accel: TranscodeHWAccel.fromJson(json[r'accel'])!,
|
||||
accelDecode: mapValueOfType<bool>(json, r'accelDecode')!,
|
||||
acceptedAudioCodecs: AudioCodec.listFromJson(json[r'acceptedAudioCodecs']),
|
||||
acceptedVideoCodecs: VideoCodec.listFromJson(json[r'acceptedVideoCodecs']),
|
||||
bframes: mapValueOfType<int>(json, r'bframes')!,
|
||||
@@ -235,6 +242,7 @@ class SystemConfigFFmpegDto {
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'accel',
|
||||
'accelDecode',
|
||||
'acceptedAudioCodecs',
|
||||
'acceptedVideoCodecs',
|
||||
'bframes',
|
||||
|
||||
@@ -21,6 +21,11 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// bool accelDecode
|
||||
test('to test the property `accelDecode`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// List<AudioCodec> acceptedAudioCodecs (default value: const [])
|
||||
test('to test the property `acceptedAudioCodecs`', () async {
|
||||
// TODO
|
||||
|
||||
@@ -61,18 +61,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: auto_route
|
||||
sha256: "82f8df1d177416bc6b7a449127d0270ff1f0f633a91f2ceb7a85d4f07c3affa1"
|
||||
sha256: "6cad3f408863ffff2b5757967c802b18415dac4acb1b40c5cdd45d0a26e5080f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.8.4"
|
||||
version: "8.1.3"
|
||||
auto_route_generator:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: auto_route_generator
|
||||
sha256: "11067a3bcd643812518fe26c0c9ec073990286cabfd9d74b6da9ef9b913c4d22"
|
||||
sha256: ba28133d3a3bf0a66772bcc98dade5843753cd9f1a8fb4802b842895515b67d3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.3.2"
|
||||
version: "8.0.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -503,10 +503,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7
|
||||
sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "4.0.0"
|
||||
flutter_local_notifications:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -896,10 +896,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
|
||||
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "4.0.0"
|
||||
logging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: 'none'
|
||||
version: 1.105.0+139
|
||||
version: 1.105.1+140
|
||||
|
||||
environment:
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
@@ -22,7 +22,7 @@ dependencies:
|
||||
cached_network_image: ^3.3.1
|
||||
flutter_cache_manager: ^3.3.1
|
||||
intl: ^0.18.0
|
||||
auto_route: ^7.8.4
|
||||
auto_route: ^8.0.2
|
||||
fluttertoast: ^8.2.4
|
||||
video_player: ^2.8.2
|
||||
chewie: ^1.7.4
|
||||
@@ -86,9 +86,9 @@ dependency_overrides:
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^3.0.1
|
||||
flutter_lints: ^4.0.0
|
||||
build_runner: ^2.4.8
|
||||
auto_route_generator: ^7.3.2
|
||||
auto_route_generator: ^8.0.0
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
flutter_native_splash: ^2.3.9
|
||||
isar_generator: ^3.1.0+1
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@Skip('currently failing due to mock HTTP client to download ISAR binaries')
|
||||
@Tags(['widget'])
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@Skip('currently failing due to mock HTTP client to download ISAR binaries')
|
||||
@Tags(['widget'])
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@Skip('currently failing due to mock HTTP client to download ISAR binaries')
|
||||
@Tags(['widget'])
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@Tags(['widget'])
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@Tags(['widget'])
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@@ -6446,7 +6446,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "1.105.0",
|
||||
"version": "1.105.1",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [],
|
||||
@@ -10002,6 +10002,9 @@
|
||||
"accel": {
|
||||
"$ref": "#/components/schemas/TranscodeHWAccel"
|
||||
},
|
||||
"accelDecode": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"acceptedAudioCodecs": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AudioCodec"
|
||||
@@ -10077,6 +10080,7 @@
|
||||
},
|
||||
"required": [
|
||||
"accel",
|
||||
"accelDecode",
|
||||
"acceptedAudioCodecs",
|
||||
"acceptedVideoCodecs",
|
||||
"bframes",
|
||||
|
||||
4
open-api/typescript-sdk/package-lock.json
generated
4
open-api/typescript-sdk/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.105.0",
|
||||
"version": "1.105.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.105.0",
|
||||
"version": "1.105.1",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.105.0",
|
||||
"version": "1.105.1",
|
||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||
"type": "module",
|
||||
"main": "./build/index.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Immich
|
||||
* 1.105.0
|
||||
* 1.105.1
|
||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||
* See https://www.npmjs.com/package/oazapfts
|
||||
*/
|
||||
@@ -863,6 +863,7 @@ export type AssetFullSyncDto = {
|
||||
};
|
||||
export type SystemConfigFFmpegDto = {
|
||||
accel: TranscodeHWAccel;
|
||||
accelDecode: boolean;
|
||||
acceptedAudioCodecs: AudioCodec[];
|
||||
acceptedVideoCodecs: VideoCodec[];
|
||||
bframes: number;
|
||||
|
||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.105.0",
|
||||
"version": "1.105.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich",
|
||||
"version": "1.105.0",
|
||||
"version": "1.105.1",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@nestjs/bullmq": "^10.0.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.105.0",
|
||||
"version": "1.105.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -1,12 +1,346 @@
|
||||
import { RegisterQueueOptions } from '@nestjs/bullmq';
|
||||
import { ConfigModuleOptions } from '@nestjs/config';
|
||||
import { CronExpression } from '@nestjs/schedule';
|
||||
import { QueueOptions } from 'bullmq';
|
||||
import { Request, Response } from 'express';
|
||||
import { RedisOptions } from 'ioredis';
|
||||
import Joi from 'joi';
|
||||
import { CLS_ID, ClsModuleOptions } from 'nestjs-cls';
|
||||
import { LogLevel } from 'src/entities/system-config.entity';
|
||||
import { QueueName } from 'src/interfaces/job.interface';
|
||||
import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface';
|
||||
|
||||
export enum TranscodePolicy {
|
||||
ALL = 'all',
|
||||
OPTIMAL = 'optimal',
|
||||
BITRATE = 'bitrate',
|
||||
REQUIRED = 'required',
|
||||
DISABLED = 'disabled',
|
||||
}
|
||||
|
||||
export enum TranscodeTarget {
|
||||
NONE,
|
||||
AUDIO,
|
||||
VIDEO,
|
||||
ALL,
|
||||
}
|
||||
|
||||
export enum VideoCodec {
|
||||
H264 = 'h264',
|
||||
HEVC = 'hevc',
|
||||
VP9 = 'vp9',
|
||||
AV1 = 'av1',
|
||||
}
|
||||
|
||||
export enum AudioCodec {
|
||||
MP3 = 'mp3',
|
||||
AAC = 'aac',
|
||||
LIBOPUS = 'libopus',
|
||||
}
|
||||
|
||||
export enum TranscodeHWAccel {
|
||||
NVENC = 'nvenc',
|
||||
QSV = 'qsv',
|
||||
VAAPI = 'vaapi',
|
||||
RKMPP = 'rkmpp',
|
||||
DISABLED = 'disabled',
|
||||
}
|
||||
|
||||
export enum ToneMapping {
|
||||
HABLE = 'hable',
|
||||
MOBIUS = 'mobius',
|
||||
REINHARD = 'reinhard',
|
||||
DISABLED = 'disabled',
|
||||
}
|
||||
|
||||
export enum CQMode {
|
||||
AUTO = 'auto',
|
||||
CQP = 'cqp',
|
||||
ICQ = 'icq',
|
||||
}
|
||||
|
||||
export enum Colorspace {
|
||||
SRGB = 'srgb',
|
||||
P3 = 'p3',
|
||||
}
|
||||
|
||||
export enum ImageFormat {
|
||||
JPEG = 'jpeg',
|
||||
WEBP = 'webp',
|
||||
}
|
||||
|
||||
export enum LogLevel {
|
||||
VERBOSE = 'verbose',
|
||||
DEBUG = 'debug',
|
||||
LOG = 'log',
|
||||
WARN = 'warn',
|
||||
ERROR = 'error',
|
||||
FATAL = 'fatal',
|
||||
}
|
||||
|
||||
export interface SystemConfig {
|
||||
ffmpeg: {
|
||||
crf: number;
|
||||
threads: number;
|
||||
preset: string;
|
||||
targetVideoCodec: VideoCodec;
|
||||
acceptedVideoCodecs: VideoCodec[];
|
||||
targetAudioCodec: AudioCodec;
|
||||
acceptedAudioCodecs: AudioCodec[];
|
||||
targetResolution: string;
|
||||
maxBitrate: string;
|
||||
bframes: number;
|
||||
refs: number;
|
||||
gopSize: number;
|
||||
npl: number;
|
||||
temporalAQ: boolean;
|
||||
cqMode: CQMode;
|
||||
twoPass: boolean;
|
||||
preferredHwDevice: string;
|
||||
transcode: TranscodePolicy;
|
||||
accel: TranscodeHWAccel;
|
||||
accelDecode: false;
|
||||
tonemap: ToneMapping;
|
||||
};
|
||||
job: Record<ConcurrentQueueName, { concurrency: number }>;
|
||||
logging: {
|
||||
enabled: boolean;
|
||||
level: LogLevel;
|
||||
};
|
||||
machineLearning: {
|
||||
enabled: boolean;
|
||||
url: string;
|
||||
clip: {
|
||||
enabled: boolean;
|
||||
modelName: string;
|
||||
};
|
||||
facialRecognition: {
|
||||
enabled: boolean;
|
||||
modelName: string;
|
||||
minScore: number;
|
||||
minFaces: number;
|
||||
maxDistance: number;
|
||||
};
|
||||
};
|
||||
map: {
|
||||
enabled: boolean;
|
||||
lightStyle: string;
|
||||
darkStyle: string;
|
||||
};
|
||||
reverseGeocoding: {
|
||||
enabled: boolean;
|
||||
};
|
||||
oauth: {
|
||||
autoLaunch: boolean;
|
||||
autoRegister: boolean;
|
||||
buttonText: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
defaultStorageQuota: number;
|
||||
enabled: boolean;
|
||||
issuerUrl: string;
|
||||
mobileOverrideEnabled: boolean;
|
||||
mobileRedirectUri: string;
|
||||
scope: string;
|
||||
signingAlgorithm: string;
|
||||
storageLabelClaim: string;
|
||||
storageQuotaClaim: string;
|
||||
};
|
||||
passwordLogin: {
|
||||
enabled: boolean;
|
||||
};
|
||||
storageTemplate: {
|
||||
enabled: boolean;
|
||||
hashVerificationEnabled: boolean;
|
||||
template: string;
|
||||
};
|
||||
image: {
|
||||
thumbnailFormat: ImageFormat;
|
||||
thumbnailSize: number;
|
||||
previewFormat: ImageFormat;
|
||||
previewSize: number;
|
||||
quality: number;
|
||||
colorspace: Colorspace;
|
||||
extractEmbedded: boolean;
|
||||
};
|
||||
newVersionCheck: {
|
||||
enabled: boolean;
|
||||
};
|
||||
trash: {
|
||||
enabled: boolean;
|
||||
days: number;
|
||||
};
|
||||
theme: {
|
||||
customCss: string;
|
||||
};
|
||||
library: {
|
||||
scan: {
|
||||
enabled: boolean;
|
||||
cronExpression: string;
|
||||
};
|
||||
watch: {
|
||||
enabled: boolean;
|
||||
};
|
||||
};
|
||||
notifications: {
|
||||
smtp: {
|
||||
enabled: boolean;
|
||||
from: string;
|
||||
replyTo: string;
|
||||
transport: {
|
||||
ignoreCert: boolean;
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
server: {
|
||||
externalDomain: string;
|
||||
loginPageMessage: string;
|
||||
};
|
||||
user: {
|
||||
deleteDelay: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const defaults = Object.freeze<SystemConfig>({
|
||||
ffmpeg: {
|
||||
crf: 23,
|
||||
threads: 0,
|
||||
preset: 'ultrafast',
|
||||
targetVideoCodec: VideoCodec.H264,
|
||||
acceptedVideoCodecs: [VideoCodec.H264],
|
||||
targetAudioCodec: AudioCodec.AAC,
|
||||
acceptedAudioCodecs: [AudioCodec.AAC, AudioCodec.MP3, AudioCodec.LIBOPUS],
|
||||
targetResolution: '720',
|
||||
maxBitrate: '0',
|
||||
bframes: -1,
|
||||
refs: 0,
|
||||
gopSize: 0,
|
||||
npl: 0,
|
||||
temporalAQ: false,
|
||||
cqMode: CQMode.AUTO,
|
||||
twoPass: false,
|
||||
preferredHwDevice: 'auto',
|
||||
transcode: TranscodePolicy.REQUIRED,
|
||||
tonemap: ToneMapping.HABLE,
|
||||
accel: TranscodeHWAccel.DISABLED,
|
||||
accelDecode: false,
|
||||
},
|
||||
job: {
|
||||
[QueueName.BACKGROUND_TASK]: { concurrency: 5 },
|
||||
[QueueName.SMART_SEARCH]: { concurrency: 2 },
|
||||
[QueueName.METADATA_EXTRACTION]: { concurrency: 5 },
|
||||
[QueueName.FACE_DETECTION]: { concurrency: 2 },
|
||||
[QueueName.SEARCH]: { concurrency: 5 },
|
||||
[QueueName.SIDECAR]: { concurrency: 5 },
|
||||
[QueueName.LIBRARY]: { concurrency: 5 },
|
||||
[QueueName.MIGRATION]: { concurrency: 5 },
|
||||
[QueueName.THUMBNAIL_GENERATION]: { concurrency: 5 },
|
||||
[QueueName.VIDEO_CONVERSION]: { concurrency: 1 },
|
||||
[QueueName.NOTIFICATION]: { concurrency: 5 },
|
||||
},
|
||||
logging: {
|
||||
enabled: true,
|
||||
level: LogLevel.LOG,
|
||||
},
|
||||
machineLearning: {
|
||||
enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false',
|
||||
url: process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003',
|
||||
clip: {
|
||||
enabled: true,
|
||||
modelName: 'ViT-B-32__openai',
|
||||
},
|
||||
facialRecognition: {
|
||||
enabled: true,
|
||||
modelName: 'buffalo_l',
|
||||
minScore: 0.7,
|
||||
maxDistance: 0.5,
|
||||
minFaces: 3,
|
||||
},
|
||||
},
|
||||
map: {
|
||||
enabled: true,
|
||||
lightStyle: '',
|
||||
darkStyle: '',
|
||||
},
|
||||
reverseGeocoding: {
|
||||
enabled: true,
|
||||
},
|
||||
oauth: {
|
||||
autoLaunch: false,
|
||||
autoRegister: true,
|
||||
buttonText: 'Login with OAuth',
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
defaultStorageQuota: 0,
|
||||
enabled: false,
|
||||
issuerUrl: '',
|
||||
mobileOverrideEnabled: false,
|
||||
mobileRedirectUri: '',
|
||||
scope: 'openid email profile',
|
||||
signingAlgorithm: 'RS256',
|
||||
storageLabelClaim: 'preferred_username',
|
||||
storageQuotaClaim: 'immich_quota',
|
||||
},
|
||||
passwordLogin: {
|
||||
enabled: true,
|
||||
},
|
||||
storageTemplate: {
|
||||
enabled: false,
|
||||
hashVerificationEnabled: true,
|
||||
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
|
||||
},
|
||||
image: {
|
||||
thumbnailFormat: ImageFormat.WEBP,
|
||||
thumbnailSize: 250,
|
||||
previewFormat: ImageFormat.JPEG,
|
||||
previewSize: 1440,
|
||||
quality: 80,
|
||||
colorspace: Colorspace.P3,
|
||||
extractEmbedded: false,
|
||||
},
|
||||
newVersionCheck: {
|
||||
enabled: true,
|
||||
},
|
||||
trash: {
|
||||
enabled: true,
|
||||
days: 30,
|
||||
},
|
||||
theme: {
|
||||
customCss: '',
|
||||
},
|
||||
library: {
|
||||
scan: {
|
||||
enabled: true,
|
||||
cronExpression: CronExpression.EVERY_DAY_AT_MIDNIGHT,
|
||||
},
|
||||
watch: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
server: {
|
||||
externalDomain: '',
|
||||
loginPageMessage: '',
|
||||
},
|
||||
notifications: {
|
||||
smtp: {
|
||||
enabled: false,
|
||||
from: '',
|
||||
replyTo: '',
|
||||
transport: {
|
||||
ignoreCert: false,
|
||||
host: '',
|
||||
port: 587,
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
deleteDelay: 7,
|
||||
},
|
||||
});
|
||||
|
||||
const WHEN_DB_URL_SET = Joi.when('DB_URL', {
|
||||
is: Joi.exist(),
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { ImageFormat } from 'src/config';
|
||||
import { APP_MEDIA_LOCATION } from 'src/constants';
|
||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AssetPathType, PathType, PersonPathType } from 'src/entities/move.entity';
|
||||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
import { ImageFormat } from 'src/entities/system-config.entity';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
|
||||
@@ -1,189 +1,19 @@
|
||||
import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common';
|
||||
import { CronExpression } from '@nestjs/schedule';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import AsyncLock from 'async-lock';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validate } from 'class-validator';
|
||||
import { load as loadYaml } from 'js-yaml';
|
||||
import * as _ from 'lodash';
|
||||
import { Subject } from 'rxjs';
|
||||
import { SystemConfig, defaults } from 'src/config';
|
||||
import { SystemConfigDto } from 'src/dtos/system-config.dto';
|
||||
import {
|
||||
AudioCodec,
|
||||
CQMode,
|
||||
Colorspace,
|
||||
ImageFormat,
|
||||
LogLevel,
|
||||
SystemConfig,
|
||||
SystemConfigEntity,
|
||||
SystemConfigKey,
|
||||
SystemConfigValue,
|
||||
ToneMapping,
|
||||
TranscodeHWAccel,
|
||||
TranscodePolicy,
|
||||
VideoCodec,
|
||||
} from 'src/entities/system-config.entity';
|
||||
import { SystemConfigEntity, SystemConfigKey, SystemConfigValue } from 'src/entities/system-config.entity';
|
||||
import { DatabaseLock } from 'src/interfaces/database.interface';
|
||||
import { QueueName } from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
|
||||
export type SystemConfigValidator = (config: SystemConfig, newConfig: SystemConfig) => void | Promise<void>;
|
||||
|
||||
export const defaults = Object.freeze<SystemConfig>({
|
||||
ffmpeg: {
|
||||
crf: 23,
|
||||
threads: 0,
|
||||
preset: 'ultrafast',
|
||||
targetVideoCodec: VideoCodec.H264,
|
||||
acceptedVideoCodecs: [VideoCodec.H264],
|
||||
targetAudioCodec: AudioCodec.AAC,
|
||||
acceptedAudioCodecs: [AudioCodec.AAC, AudioCodec.MP3, AudioCodec.LIBOPUS],
|
||||
targetResolution: '720',
|
||||
maxBitrate: '0',
|
||||
bframes: -1,
|
||||
refs: 0,
|
||||
gopSize: 0,
|
||||
npl: 0,
|
||||
temporalAQ: false,
|
||||
cqMode: CQMode.AUTO,
|
||||
twoPass: false,
|
||||
preferredHwDevice: 'auto',
|
||||
transcode: TranscodePolicy.REQUIRED,
|
||||
tonemap: ToneMapping.HABLE,
|
||||
accel: TranscodeHWAccel.DISABLED,
|
||||
},
|
||||
job: {
|
||||
[QueueName.BACKGROUND_TASK]: { concurrency: 5 },
|
||||
[QueueName.SMART_SEARCH]: { concurrency: 2 },
|
||||
[QueueName.METADATA_EXTRACTION]: { concurrency: 5 },
|
||||
[QueueName.FACE_DETECTION]: { concurrency: 2 },
|
||||
[QueueName.SEARCH]: { concurrency: 5 },
|
||||
[QueueName.SIDECAR]: { concurrency: 5 },
|
||||
[QueueName.LIBRARY]: { concurrency: 5 },
|
||||
[QueueName.MIGRATION]: { concurrency: 5 },
|
||||
[QueueName.THUMBNAIL_GENERATION]: { concurrency: 5 },
|
||||
[QueueName.VIDEO_CONVERSION]: { concurrency: 1 },
|
||||
[QueueName.NOTIFICATION]: { concurrency: 5 },
|
||||
},
|
||||
logging: {
|
||||
enabled: true,
|
||||
level: LogLevel.LOG,
|
||||
},
|
||||
machineLearning: {
|
||||
enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false',
|
||||
url: process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003',
|
||||
clip: {
|
||||
enabled: true,
|
||||
modelName: 'ViT-B-32__openai',
|
||||
},
|
||||
facialRecognition: {
|
||||
enabled: true,
|
||||
modelName: 'buffalo_l',
|
||||
minScore: 0.7,
|
||||
maxDistance: 0.5,
|
||||
minFaces: 3,
|
||||
},
|
||||
},
|
||||
map: {
|
||||
enabled: true,
|
||||
lightStyle: '',
|
||||
darkStyle: '',
|
||||
},
|
||||
reverseGeocoding: {
|
||||
enabled: true,
|
||||
},
|
||||
oauth: {
|
||||
autoLaunch: false,
|
||||
autoRegister: true,
|
||||
buttonText: 'Login with OAuth',
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
defaultStorageQuota: 0,
|
||||
enabled: false,
|
||||
issuerUrl: '',
|
||||
mobileOverrideEnabled: false,
|
||||
mobileRedirectUri: '',
|
||||
scope: 'openid email profile',
|
||||
signingAlgorithm: 'RS256',
|
||||
storageLabelClaim: 'preferred_username',
|
||||
storageQuotaClaim: 'immich_quota',
|
||||
},
|
||||
passwordLogin: {
|
||||
enabled: true,
|
||||
},
|
||||
storageTemplate: {
|
||||
enabled: false,
|
||||
hashVerificationEnabled: true,
|
||||
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
|
||||
},
|
||||
image: {
|
||||
thumbnailFormat: ImageFormat.WEBP,
|
||||
thumbnailSize: 250,
|
||||
previewFormat: ImageFormat.JPEG,
|
||||
previewSize: 1440,
|
||||
quality: 80,
|
||||
colorspace: Colorspace.P3,
|
||||
extractEmbedded: false,
|
||||
},
|
||||
newVersionCheck: {
|
||||
enabled: true,
|
||||
},
|
||||
trash: {
|
||||
enabled: true,
|
||||
days: 30,
|
||||
},
|
||||
theme: {
|
||||
customCss: '',
|
||||
},
|
||||
library: {
|
||||
scan: {
|
||||
enabled: true,
|
||||
cronExpression: CronExpression.EVERY_DAY_AT_MIDNIGHT,
|
||||
},
|
||||
watch: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
server: {
|
||||
externalDomain: '',
|
||||
loginPageMessage: '',
|
||||
},
|
||||
notifications: {
|
||||
smtp: {
|
||||
enabled: false,
|
||||
from: '',
|
||||
replyTo: '',
|
||||
transport: {
|
||||
ignoreCert: false,
|
||||
host: '',
|
||||
port: 587,
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
deleteDelay: 7,
|
||||
},
|
||||
});
|
||||
|
||||
export enum FeatureFlag {
|
||||
SMART_SEARCH = 'smartSearch',
|
||||
FACIAL_RECOGNITION = 'facialRecognition',
|
||||
MAP = 'map',
|
||||
REVERSE_GEOCODING = 'reverseGeocoding',
|
||||
SIDECAR = 'sidecar',
|
||||
SEARCH = 'search',
|
||||
OAUTH = 'oauth',
|
||||
OAUTH_AUTO_LAUNCH = 'oauthAutoLaunch',
|
||||
PASSWORD_LOGIN = 'passwordLogin',
|
||||
CONFIG_FILE = 'configFile',
|
||||
TRASH = 'trash',
|
||||
EMAIL = 'email',
|
||||
}
|
||||
|
||||
export type FeatureFlags = Record<FeatureFlag, boolean>;
|
||||
|
||||
let instance: SystemConfigCore | null;
|
||||
|
||||
@Injectable()
|
||||
@@ -192,7 +22,7 @@ export class SystemConfigCore {
|
||||
private config: SystemConfig | null = null;
|
||||
private lastUpdated: number | null = null;
|
||||
|
||||
public config$ = new Subject<SystemConfig>();
|
||||
config$ = new Subject<SystemConfig>();
|
||||
|
||||
private constructor(
|
||||
private repository: ISystemConfigRepository,
|
||||
@@ -210,68 +40,7 @@ export class SystemConfigCore {
|
||||
instance = null;
|
||||
}
|
||||
|
||||
async requireFeature(feature: FeatureFlag) {
|
||||
const hasFeature = await this.hasFeature(feature);
|
||||
if (!hasFeature) {
|
||||
switch (feature) {
|
||||
case FeatureFlag.SMART_SEARCH: {
|
||||
throw new BadRequestException('Smart search is not enabled');
|
||||
}
|
||||
case FeatureFlag.FACIAL_RECOGNITION: {
|
||||
throw new BadRequestException('Facial recognition is not enabled');
|
||||
}
|
||||
case FeatureFlag.SIDECAR: {
|
||||
throw new BadRequestException('Sidecar is not enabled');
|
||||
}
|
||||
case FeatureFlag.SEARCH: {
|
||||
throw new BadRequestException('Search is not enabled');
|
||||
}
|
||||
case FeatureFlag.OAUTH: {
|
||||
throw new BadRequestException('OAuth is not enabled');
|
||||
}
|
||||
case FeatureFlag.PASSWORD_LOGIN: {
|
||||
throw new BadRequestException('Password login is not enabled');
|
||||
}
|
||||
case FeatureFlag.CONFIG_FILE: {
|
||||
throw new BadRequestException('Config file is not set');
|
||||
}
|
||||
default: {
|
||||
throw new ForbiddenException(`Missing required feature: ${feature}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async hasFeature(feature: FeatureFlag) {
|
||||
const features = await this.getFeatures();
|
||||
return features[feature] ?? false;
|
||||
}
|
||||
|
||||
async getFeatures(): Promise<FeatureFlags> {
|
||||
const config = await this.getConfig();
|
||||
const mlEnabled = config.machineLearning.enabled;
|
||||
|
||||
return {
|
||||
[FeatureFlag.SMART_SEARCH]: mlEnabled && config.machineLearning.clip.enabled,
|
||||
[FeatureFlag.FACIAL_RECOGNITION]: mlEnabled && config.machineLearning.facialRecognition.enabled,
|
||||
[FeatureFlag.MAP]: config.map.enabled,
|
||||
[FeatureFlag.REVERSE_GEOCODING]: config.reverseGeocoding.enabled,
|
||||
[FeatureFlag.SIDECAR]: true,
|
||||
[FeatureFlag.SEARCH]: true,
|
||||
[FeatureFlag.TRASH]: config.trash.enabled,
|
||||
[FeatureFlag.OAUTH]: config.oauth.enabled,
|
||||
[FeatureFlag.OAUTH_AUTO_LAUNCH]: config.oauth.autoLaunch,
|
||||
[FeatureFlag.PASSWORD_LOGIN]: config.passwordLogin.enabled,
|
||||
[FeatureFlag.CONFIG_FILE]: !!process.env.IMMICH_CONFIG_FILE,
|
||||
[FeatureFlag.EMAIL]: config.notifications.smtp.enabled,
|
||||
};
|
||||
}
|
||||
|
||||
public getDefaults(): SystemConfig {
|
||||
return defaults;
|
||||
}
|
||||
|
||||
public async getConfig(force = false): Promise<SystemConfig> {
|
||||
async getConfig(force = false): Promise<SystemConfig> {
|
||||
if (force || !this.config) {
|
||||
const lastUpdated = this.lastUpdated;
|
||||
await this.asyncLock.acquire(DatabaseLock[DatabaseLock.GetSystemConfig], async () => {
|
||||
@@ -285,11 +54,7 @@ export class SystemConfigCore {
|
||||
return this.config!;
|
||||
}
|
||||
|
||||
public async updateConfig(newConfig: SystemConfig): Promise<SystemConfig> {
|
||||
if (await this.hasFeature(FeatureFlag.CONFIG_FILE)) {
|
||||
throw new BadRequestException('Cannot update configuration while IMMICH_CONFIG_FILE is in use');
|
||||
}
|
||||
|
||||
async updateConfig(newConfig: SystemConfig): Promise<SystemConfig> {
|
||||
const updates: SystemConfigEntity[] = [];
|
||||
const deletes: SystemConfigEntity[] = [];
|
||||
|
||||
@@ -328,16 +93,19 @@ export class SystemConfigCore {
|
||||
return config;
|
||||
}
|
||||
|
||||
public async refreshConfig() {
|
||||
async refreshConfig() {
|
||||
const newConfig = await this.getConfig(true);
|
||||
|
||||
this.config$.next(newConfig);
|
||||
}
|
||||
|
||||
isUsingConfigFile() {
|
||||
return !!process.env.IMMICH_CONFIG_FILE;
|
||||
}
|
||||
|
||||
private async buildConfig() {
|
||||
const config = _.cloneDeep(defaults);
|
||||
const overrides = process.env.IMMICH_CONFIG_FILE
|
||||
? await this.loadFromFile(process.env.IMMICH_CONFIG_FILE)
|
||||
const overrides = this.isUsingConfigFile()
|
||||
? await this.loadFromFile(process.env.IMMICH_CONFIG_FILE as string)
|
||||
: await this.repository.load();
|
||||
|
||||
for (const { key, value } of overrides) {
|
||||
@@ -347,7 +115,7 @@ export class SystemConfigCore {
|
||||
|
||||
const errors = await validate(plainToInstance(SystemConfigDto, config));
|
||||
if (errors.length > 0) {
|
||||
if (process.env.IMMICH_CONFIG_FILE) {
|
||||
if (this.isUsingConfigFile()) {
|
||||
throw new Error(`Invalid value(s) in file: ${errors}`);
|
||||
} else {
|
||||
this.logger.error('Validation error', errors);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger';
|
||||
import type { DateTime } from 'luxon';
|
||||
import { FeatureFlags } from 'src/cores/system-config.core';
|
||||
import { SystemConfigThemeDto } from 'src/dtos/system-config.dto';
|
||||
import { IVersion, VersionType } from 'src/utils/version';
|
||||
|
||||
@@ -96,7 +95,7 @@ export class ServerConfigDto {
|
||||
externalDomain!: string;
|
||||
}
|
||||
|
||||
export class ServerFeaturesDto implements FeatureFlags {
|
||||
export class ServerFeaturesDto {
|
||||
smartSearch!: boolean;
|
||||
configFile!: boolean;
|
||||
facialRecognition!: boolean;
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
ValidatorConstraint,
|
||||
ValidatorConstraintInterface,
|
||||
} from 'class-validator';
|
||||
import { CLIPConfig, RecognitionConfig } from 'src/dtos/model-config.dto';
|
||||
import {
|
||||
AudioCodec,
|
||||
CQMode,
|
||||
@@ -30,7 +29,8 @@ import {
|
||||
TranscodeHWAccel,
|
||||
TranscodePolicy,
|
||||
VideoCodec,
|
||||
} from 'src/entities/system-config.entity';
|
||||
} from 'src/config';
|
||||
import { CLIPConfig, RecognitionConfig } from 'src/dtos/model-config.dto';
|
||||
import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface';
|
||||
import { ValidateBoolean, validateCronExpression } from 'src/validation';
|
||||
|
||||
@@ -132,6 +132,9 @@ export class SystemConfigFFmpegDto {
|
||||
@ApiProperty({ enumName: 'TranscodeHWAccel', enum: TranscodeHWAccel })
|
||||
accel!: TranscodeHWAccel;
|
||||
|
||||
@ValidateBoolean()
|
||||
accelDecode!: boolean;
|
||||
|
||||
@IsEnum(ToneMapping)
|
||||
@ApiProperty({ enumName: 'ToneMapping', enum: ToneMapping })
|
||||
tonemap!: ToneMapping;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ConcurrentQueueName } from 'src/interfaces/job.interface';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
|
||||
export type SystemConfigValue = string | string[] | number | boolean;
|
||||
@@ -43,6 +43,7 @@ export const SystemConfigKey = {
|
||||
FFMPEG_PREFERRED_HW_DEVICE: 'ffmpeg.preferredHwDevice',
|
||||
FFMPEG_TRANSCODE: 'ffmpeg.transcode',
|
||||
FFMPEG_ACCEL: 'ffmpeg.accel',
|
||||
FFMPEG_ACCEL_DECODE: 'ffmpeg.accelDecode',
|
||||
FFMPEG_TONEMAP: 'ffmpeg.tonemap',
|
||||
|
||||
JOB_THUMBNAIL_GENERATION_CONCURRENCY: 'job.thumbnailGeneration.concurrency',
|
||||
@@ -143,197 +144,3 @@ export class SystemConfigEntity<T = SystemConfigValue> {
|
||||
@Column({ type: 'varchar', nullable: true, transformer: { to: JSON.stringify, from: JSON.parse } })
|
||||
value!: T;
|
||||
}
|
||||
|
||||
export enum TranscodePolicy {
|
||||
ALL = 'all',
|
||||
OPTIMAL = 'optimal',
|
||||
BITRATE = 'bitrate',
|
||||
REQUIRED = 'required',
|
||||
DISABLED = 'disabled',
|
||||
}
|
||||
|
||||
export enum TranscodeTarget {
|
||||
NONE,
|
||||
AUDIO,
|
||||
VIDEO,
|
||||
ALL,
|
||||
}
|
||||
|
||||
export enum VideoCodec {
|
||||
H264 = 'h264',
|
||||
HEVC = 'hevc',
|
||||
VP9 = 'vp9',
|
||||
AV1 = 'av1',
|
||||
}
|
||||
|
||||
export enum AudioCodec {
|
||||
MP3 = 'mp3',
|
||||
AAC = 'aac',
|
||||
LIBOPUS = 'libopus',
|
||||
}
|
||||
|
||||
export enum TranscodeHWAccel {
|
||||
NVENC = 'nvenc',
|
||||
QSV = 'qsv',
|
||||
VAAPI = 'vaapi',
|
||||
RKMPP = 'rkmpp',
|
||||
DISABLED = 'disabled',
|
||||
}
|
||||
|
||||
export enum ToneMapping {
|
||||
HABLE = 'hable',
|
||||
MOBIUS = 'mobius',
|
||||
REINHARD = 'reinhard',
|
||||
DISABLED = 'disabled',
|
||||
}
|
||||
|
||||
export enum CQMode {
|
||||
AUTO = 'auto',
|
||||
CQP = 'cqp',
|
||||
ICQ = 'icq',
|
||||
}
|
||||
|
||||
export enum Colorspace {
|
||||
SRGB = 'srgb',
|
||||
P3 = 'p3',
|
||||
}
|
||||
|
||||
export enum ImageFormat {
|
||||
JPEG = 'jpeg',
|
||||
WEBP = 'webp',
|
||||
}
|
||||
|
||||
export enum LogLevel {
|
||||
VERBOSE = 'verbose',
|
||||
DEBUG = 'debug',
|
||||
LOG = 'log',
|
||||
WARN = 'warn',
|
||||
ERROR = 'error',
|
||||
FATAL = 'fatal',
|
||||
}
|
||||
|
||||
export interface SystemConfig {
|
||||
ffmpeg: {
|
||||
crf: number;
|
||||
threads: number;
|
||||
preset: string;
|
||||
targetVideoCodec: VideoCodec;
|
||||
acceptedVideoCodecs: VideoCodec[];
|
||||
targetAudioCodec: AudioCodec;
|
||||
acceptedAudioCodecs: AudioCodec[];
|
||||
targetResolution: string;
|
||||
maxBitrate: string;
|
||||
bframes: number;
|
||||
refs: number;
|
||||
gopSize: number;
|
||||
npl: number;
|
||||
temporalAQ: boolean;
|
||||
cqMode: CQMode;
|
||||
twoPass: boolean;
|
||||
preferredHwDevice: string;
|
||||
transcode: TranscodePolicy;
|
||||
accel: TranscodeHWAccel;
|
||||
tonemap: ToneMapping;
|
||||
};
|
||||
job: Record<ConcurrentQueueName, { concurrency: number }>;
|
||||
logging: {
|
||||
enabled: boolean;
|
||||
level: LogLevel;
|
||||
};
|
||||
machineLearning: {
|
||||
enabled: boolean;
|
||||
url: string;
|
||||
clip: {
|
||||
enabled: boolean;
|
||||
modelName: string;
|
||||
};
|
||||
facialRecognition: {
|
||||
enabled: boolean;
|
||||
modelName: string;
|
||||
minScore: number;
|
||||
minFaces: number;
|
||||
maxDistance: number;
|
||||
};
|
||||
};
|
||||
map: {
|
||||
enabled: boolean;
|
||||
lightStyle: string;
|
||||
darkStyle: string;
|
||||
};
|
||||
reverseGeocoding: {
|
||||
enabled: boolean;
|
||||
};
|
||||
oauth: {
|
||||
autoLaunch: boolean;
|
||||
autoRegister: boolean;
|
||||
buttonText: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
defaultStorageQuota: number;
|
||||
enabled: boolean;
|
||||
issuerUrl: string;
|
||||
mobileOverrideEnabled: boolean;
|
||||
mobileRedirectUri: string;
|
||||
scope: string;
|
||||
signingAlgorithm: string;
|
||||
storageLabelClaim: string;
|
||||
storageQuotaClaim: string;
|
||||
};
|
||||
passwordLogin: {
|
||||
enabled: boolean;
|
||||
};
|
||||
storageTemplate: {
|
||||
enabled: boolean;
|
||||
hashVerificationEnabled: boolean;
|
||||
template: string;
|
||||
};
|
||||
image: {
|
||||
thumbnailFormat: ImageFormat;
|
||||
thumbnailSize: number;
|
||||
previewFormat: ImageFormat;
|
||||
previewSize: number;
|
||||
quality: number;
|
||||
colorspace: Colorspace;
|
||||
extractEmbedded: boolean;
|
||||
};
|
||||
newVersionCheck: {
|
||||
enabled: boolean;
|
||||
};
|
||||
trash: {
|
||||
enabled: boolean;
|
||||
days: number;
|
||||
};
|
||||
theme: {
|
||||
customCss: string;
|
||||
};
|
||||
library: {
|
||||
scan: {
|
||||
enabled: boolean;
|
||||
cronExpression: string;
|
||||
};
|
||||
watch: {
|
||||
enabled: boolean;
|
||||
};
|
||||
};
|
||||
notifications: {
|
||||
smtp: {
|
||||
enabled: boolean;
|
||||
from: string;
|
||||
replyTo: string;
|
||||
transport: {
|
||||
ignoreCert: boolean;
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
server: {
|
||||
externalDomain: string;
|
||||
loginPageMessage: string;
|
||||
};
|
||||
user: {
|
||||
deleteDelay: number;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server-info.dto';
|
||||
import { SystemConfig } from 'src/entities/system-config.entity';
|
||||
|
||||
export const IEventRepository = 'IEventRepository';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LogLevel } from 'src/entities/system-config.entity';
|
||||
import { LogLevel } from 'src/config';
|
||||
|
||||
export const ILoggerRepository = 'ILoggerRepository';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Writable } from 'node:stream';
|
||||
import { ImageFormat, TranscodeTarget, VideoCodec } from 'src/entities/system-config.entity';
|
||||
import { ImageFormat, TranscodeTarget, VideoCodec } from 'src/config';
|
||||
|
||||
export const IMediaRepository = 'IMediaRepository';
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import { existsSync } from 'node:fs';
|
||||
import { Worker } from 'node:worker_threads';
|
||||
import sirv from 'sirv';
|
||||
import { ApiModule, ImmichAdminModule } from 'src/app.module';
|
||||
import { LogLevel } from 'src/config';
|
||||
import { WEB_ROOT, envName, excludePaths, isDev, serverVersion } from 'src/constants';
|
||||
import { LogLevel } from 'src/entities/system-config.entity';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
|
||||
import { ApiService } from 'src/services/api.service';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ConsoleLogger, Injectable, Scope } from '@nestjs/common';
|
||||
import { isLogLevelEnabled } from '@nestjs/common/services/utils/is-log-level-enabled.util';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { LogLevel } from 'src/entities/system-config.entity';
|
||||
import { LogLevel } from 'src/config';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { LogColor } from 'src/utils/logger-colors';
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import fs from 'node:fs/promises';
|
||||
import { Writable } from 'node:stream';
|
||||
import { promisify } from 'node:util';
|
||||
import sharp from 'sharp';
|
||||
import { Colorspace } from 'src/entities/system-config.entity';
|
||||
import { Colorspace } from 'src/config';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import {
|
||||
IMediaRepository,
|
||||
|
||||
@@ -434,12 +434,13 @@ export class AssetService {
|
||||
await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id: asset.livePhotoVideoId } });
|
||||
}
|
||||
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.DELETE_FILES,
|
||||
data: {
|
||||
files: [asset.thumbnailPath, asset.previewPath, asset.encodedVideoPath, asset.sidecarPath, asset.originalPath],
|
||||
},
|
||||
});
|
||||
const files = [asset.thumbnailPath, asset.previewPath, asset.encodedVideoPath];
|
||||
// skip originals if the user deleted the whole library
|
||||
if (!asset.library.deletedAt) {
|
||||
files.push(asset.sidecarPath, asset.originalPath);
|
||||
}
|
||||
|
||||
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files } });
|
||||
|
||||
return JobStatus.SUCCESS;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import cookieParser from 'cookie';
|
||||
import { DateTime } from 'luxon';
|
||||
import { IncomingHttpHeaders } from 'node:http';
|
||||
import { ClientMetadata, Issuer, UserinfoResponse, custom, generators } from 'openid-client';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { AuthType, LOGIN_URL, MOBILE_REDIRECT } from 'src/constants';
|
||||
import { AccessCore } from 'src/cores/access.core';
|
||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
@@ -28,7 +29,6 @@ import {
|
||||
mapLoginResponse,
|
||||
} from 'src/dtos/auth.dto';
|
||||
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
||||
import { SystemConfig } from 'src/entities/system-config.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { FeatureFlag, SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import { SystemConfig, SystemConfigKey, SystemConfigKeyPaths } from 'src/entities/system-config.entity';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import {
|
||||
@@ -367,32 +367,5 @@ describe(JobService.name, () => {
|
||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||
});
|
||||
}
|
||||
|
||||
const featureTests: Array<{ queue: QueueName; feature: FeatureFlag; configKey: SystemConfigKeyPaths }> = [
|
||||
{
|
||||
queue: QueueName.SMART_SEARCH,
|
||||
feature: FeatureFlag.SMART_SEARCH,
|
||||
configKey: SystemConfigKey.MACHINE_LEARNING_CLIP_ENABLED,
|
||||
},
|
||||
{
|
||||
queue: QueueName.FACE_DETECTION,
|
||||
feature: FeatureFlag.FACIAL_RECOGNITION,
|
||||
configKey: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_ENABLED,
|
||||
},
|
||||
{
|
||||
queue: QueueName.FACIAL_RECOGNITION,
|
||||
feature: FeatureFlag.FACIAL_RECOGNITION,
|
||||
configKey: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_ENABLED,
|
||||
},
|
||||
];
|
||||
|
||||
for (const { queue, feature, configKey } of featureTests) {
|
||||
it(`should throw an error if attempting to queue ${queue} when ${feature} is disabled`, async () => {
|
||||
configMock.load.mockResolvedValue([{ key: configKey, value: false }]);
|
||||
jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
|
||||
|
||||
await expect(sut.handleCommand(queue, { command: JobCommand.START, force: false })).rejects.toThrow();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { snakeCase } from 'lodash';
|
||||
import { FeatureFlag, SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AllJobStatusResponseDto, JobCommandDto, JobStatusDto } from 'src/dtos/job.dto';
|
||||
import { AssetType } from 'src/entities/asset.entity';
|
||||
@@ -112,7 +112,6 @@ export class JobService {
|
||||
}
|
||||
|
||||
case QueueName.SMART_SEARCH: {
|
||||
await this.configCore.requireFeature(FeatureFlag.SMART_SEARCH);
|
||||
return this.jobRepository.queue({ name: JobName.QUEUE_SMART_SEARCH, data: { force } });
|
||||
}
|
||||
|
||||
@@ -121,7 +120,6 @@ export class JobService {
|
||||
}
|
||||
|
||||
case QueueName.SIDECAR: {
|
||||
await this.configCore.requireFeature(FeatureFlag.SIDECAR);
|
||||
return this.jobRepository.queue({ name: JobName.QUEUE_SIDECAR, data: { force } });
|
||||
}
|
||||
|
||||
@@ -130,12 +128,10 @@ export class JobService {
|
||||
}
|
||||
|
||||
case QueueName.FACE_DETECTION: {
|
||||
await this.configCore.requireFeature(FeatureFlag.FACIAL_RECOGNITION);
|
||||
return this.jobRepository.queue({ name: JobName.QUEUE_FACE_DETECTION, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.FACIAL_RECOGNITION: {
|
||||
await this.configCore.requireFeature(FeatureFlag.FACIAL_RECOGNITION);
|
||||
return this.jobRepository.queue({ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force } });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { Stats } from 'node:fs';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import { mapLibrary } from 'src/dtos/library.dto';
|
||||
import { AssetType } from 'src/entities/asset.entity';
|
||||
import { LibraryType } from 'src/entities/library.entity';
|
||||
import { SystemConfig, SystemConfigKey } from 'src/entities/system-config.entity';
|
||||
import { SystemConfigKey } from 'src/entities/system-config.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Stats } from 'node:fs';
|
||||
import { AssetType } from 'src/entities/asset.entity';
|
||||
import { ExifEntity } from 'src/entities/exif.entity';
|
||||
import {
|
||||
AudioCodec,
|
||||
Colorspace,
|
||||
ImageFormat,
|
||||
SystemConfigKey,
|
||||
ToneMapping,
|
||||
TranscodeHWAccel,
|
||||
TranscodePolicy,
|
||||
VideoCodec,
|
||||
} from 'src/entities/system-config.entity';
|
||||
} from 'src/config';
|
||||
import { AssetType } from 'src/entities/asset.entity';
|
||||
import { ExifEntity } from 'src/entities/exif.entity';
|
||||
import { SystemConfigKey } from 'src/entities/system-config.entity';
|
||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
@@ -1381,6 +1381,57 @@ describe(MediaService.name, () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should use hardware decoding for nvenc if enabled', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.NVENC },
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL_DECODE, value: true },
|
||||
]);
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
'/original/path.ext',
|
||||
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||
{
|
||||
inputOptions: expect.arrayContaining([
|
||||
'-hwaccel cuda',
|
||||
'-hwaccel_output_format cuda',
|
||||
'-noautorotate',
|
||||
'-threads 1',
|
||||
]),
|
||||
outputOptions: expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
'scale_cuda=-2:720,hwupload=derive_device=vulkan,libplacebo=color_primaries=bt709:color_trc=bt709:colorspace=bt709:deband=true:deband_iterations=3:deband_radius=8:deband_threshold=6:downscaler=none:format=yuv420p:tonemapping=clip:upscaler=none,hwupload=derive_device=cuda',
|
||||
),
|
||||
]),
|
||||
twoPass: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should use hardware tone-mapping for nvenc if hardware decoding is enabled and should tone map', async () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.NVENC },
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL_DECODE, value: true },
|
||||
]);
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
'/original/path.ext',
|
||||
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||
{
|
||||
inputOptions: expect.arrayContaining(['-hwaccel cuda', '-hwaccel_output_format cuda']),
|
||||
outputOptions: expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
'hwupload=derive_device=vulkan,libplacebo=color_primaries=bt709:color_trc=bt709:colorspace=bt709:deband=true:deband_iterations=3:deband_radius=8:deband_threshold=6:downscaler=none:format=yuv420p:tonemapping=hable:upscaler=none,hwupload=derive_device=cuda',
|
||||
),
|
||||
]),
|
||||
twoPass: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should set options for qsv', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
@@ -1693,8 +1744,12 @@ describe(MediaService.name, () => {
|
||||
|
||||
it('should set options for rkmpp', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true });
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.RKMPP }]);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.RKMPP },
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL_DECODE, value: true },
|
||||
]);
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
@@ -1723,9 +1778,11 @@ describe(MediaService.name, () => {
|
||||
|
||||
it('should set vbr options for rkmpp when max bitrate is enabled', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true });
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.RKMPP },
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL_DECODE, value: true },
|
||||
{ key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '10000k' },
|
||||
{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.HEVC },
|
||||
]);
|
||||
@@ -1744,9 +1801,11 @@ describe(MediaService.name, () => {
|
||||
|
||||
it('should set cqp options for rkmpp when max bitrate is disabled', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true });
|
||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.RKMPP },
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL_DECODE, value: true },
|
||||
{ key: SystemConfigKey.FFMPEG_CRF, value: 30 },
|
||||
{ key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '0' },
|
||||
]);
|
||||
@@ -1769,6 +1828,7 @@ describe(MediaService.name, () => {
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.RKMPP },
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL_DECODE, value: true },
|
||||
{ key: SystemConfigKey.FFMPEG_CRF, value: 30 },
|
||||
{ key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '0' },
|
||||
]);
|
||||
@@ -1788,6 +1848,60 @@ describe(MediaService.name, () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should use software decoding and tone-mapping if hardware decoding is disabled', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => true, isCharacterDevice: () => true });
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.RKMPP },
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL_DECODE, value: false },
|
||||
{ key: SystemConfigKey.FFMPEG_CRF, value: 30 },
|
||||
{ key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '0' },
|
||||
]);
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
'/original/path.ext',
|
||||
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||
{
|
||||
inputOptions: [],
|
||||
outputOptions: expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
'zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p',
|
||||
),
|
||||
]),
|
||||
twoPass: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should use software decoding and tone-mapping if opencl is not available', async () => {
|
||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||
storageMock.stat.mockResolvedValue({ ...new Stats(), isFile: () => false, isCharacterDevice: () => false });
|
||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL, value: TranscodeHWAccel.RKMPP },
|
||||
{ key: SystemConfigKey.FFMPEG_ACCEL_DECODE, value: true },
|
||||
{ key: SystemConfigKey.FFMPEG_CRF, value: 30 },
|
||||
{ key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '0' },
|
||||
]);
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||
'/original/path.ext',
|
||||
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||
{
|
||||
inputOptions: [],
|
||||
outputOptions: expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
'zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p',
|
||||
),
|
||||
]),
|
||||
twoPass: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should tonemap when policy is required and video is hdr', async () => {
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common';
|
||||
import { dirname } from 'node:path';
|
||||
import { GeneratedImageType, StorageCore, StorageFolder } from 'src/cores/storage.core';
|
||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
||||
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
|
||||
import { AssetPathType } from 'src/entities/move.entity';
|
||||
import {
|
||||
AudioCodec,
|
||||
Colorspace,
|
||||
@@ -13,7 +8,12 @@ import {
|
||||
TranscodePolicy,
|
||||
TranscodeTarget,
|
||||
VideoCodec,
|
||||
} from 'src/entities/system-config.entity';
|
||||
} from 'src/config';
|
||||
import { GeneratedImageType, StorageCore, StorageFolder } from 'src/cores/storage.core';
|
||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
||||
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
|
||||
import { AssetPathType } from 'src/entities/move.entity';
|
||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import {
|
||||
@@ -36,9 +36,11 @@ import {
|
||||
AV1Config,
|
||||
H264Config,
|
||||
HEVCConfig,
|
||||
NVENCConfig,
|
||||
NvencHwDecodeConfig,
|
||||
NvencSwDecodeConfig,
|
||||
QSVConfig,
|
||||
RKMPPConfig,
|
||||
RkmppHwDecodeConfig,
|
||||
RkmppSwDecodeConfig,
|
||||
ThumbnailConfig,
|
||||
VAAPIConfig,
|
||||
VP9Config,
|
||||
@@ -361,8 +363,7 @@ export class MediaService {
|
||||
`Error occurred during transcoding. Retrying with ${config.accel.toUpperCase()} acceleration disabled.`,
|
||||
);
|
||||
}
|
||||
config.accel = TranscodeHWAccel.DISABLED;
|
||||
transcodeOptions = await this.getCodecConfig(config).then((c) =>
|
||||
transcodeOptions = await this.getCodecConfig({ ...config, accel: TranscodeHWAccel.DISABLED }).then((c) =>
|
||||
c.getOptions(target, mainVideoStream, mainAudioStream),
|
||||
);
|
||||
await this.mediaRepository.transcode(input, output, transcodeOptions);
|
||||
@@ -495,7 +496,7 @@ export class MediaService {
|
||||
let handler: VideoCodecHWConfig;
|
||||
switch (config.accel) {
|
||||
case TranscodeHWAccel.NVENC: {
|
||||
handler = new NVENCConfig(config);
|
||||
handler = config.accelDecode ? new NvencHwDecodeConfig(config) : new NvencSwDecodeConfig(config);
|
||||
break;
|
||||
}
|
||||
case TranscodeHWAccel.QSV: {
|
||||
@@ -507,7 +508,10 @@ export class MediaService {
|
||||
break;
|
||||
}
|
||||
case TranscodeHWAccel.RKMPP: {
|
||||
handler = new RKMPPConfig(config, await this.getDevices(), await this.hasOpenCL());
|
||||
handler =
|
||||
config.accelDecode && (await this.hasOpenCL())
|
||||
? new RkmppHwDecodeConfig(config, await this.getDevices())
|
||||
: new RkmppSwDecodeConfig(config, await this.getDevices());
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user