Compare commits

...

28 Commits

Author SHA1 Message Date
mertalev
ea33cd9515 remove version flag 2024-05-14 20:40:36 -04:00
mertalev
e9ceb8b017 fix mutating config 2024-05-14 20:40:36 -04:00
mertalev
94030d809f add hw decode toggle 2024-05-14 20:40:36 -04:00
mertalev
4db0717a08 update api 2024-05-14 20:40:36 -04:00
mertalev
b6808c1675 separate configs for hw/sw 2024-05-14 20:40:36 -04:00
mertalev
a2b7403978 fix software tone-mapping not being applied 2024-05-14 20:40:36 -04:00
mertalev
1556d978ed toggle for hardware decoding, software / hardware decoding for nvenc and rkmpp 2024-05-14 20:40:36 -04:00
mertalev
48a71ac4d9 refactor 2024-05-14 20:38:08 -04:00
mertalev
adf620331c tweak settings 2024-05-14 20:38:08 -04:00
mertalev
c92637df80 update nvenc options 2024-05-14 20:38:08 -04:00
mertalev
3356b023c5 tweaks 2024-05-14 20:38:08 -04:00
mertalev
a2e8b657e6 libplacebo for nvenc
update dockerfile
2024-05-14 20:38:08 -04:00
mertalev
f420befc15 use arrayContaining 2024-05-14 20:37:56 -04:00
renovate[bot]
efb844c6cd fix(deps): update dependency @zoom-image/svelte to v0.2.12 (#9487)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-14 19:33:53 -04:00
Alex The Bot
88d4338348 Version v1.105.1 2024-05-14 21:31:24 +00:00
Jason Rasmussen
ce7bbe88f9 fix(server): skip originals when deleting a library (#9496) 2024-05-14 16:29:57 -05:00
kleinMaggus
d62e90424e feat(web,mobile) Allow videos to be looped in the detail viewer (#8615)
* First version of video looping for the web

* Use prop for slideshow state

* refactor asset settings and add autoloop video setting

* rename variables and adjust description

* loop videos based on user settings in gallery viewer

* make asset viewer setting a stateless widget

* do not update video playback value if looping is enabled

* add some translations

* adjust description

* add missing id

* WIP

* chore: clean up

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-05-14 14:31:47 -05:00
Jason Rasmussen
0f129cae4a refactor(server): feature flags (#9492) 2024-05-14 15:31:36 -04:00
renovate[bot]
5583635947 chore(deps): update python:3.11-bookworm docker digest to 96de1ea (#9490) 2024-05-14 19:13:56 +00:00
renovate[bot]
b7715305b3 chore(deps): update dependency fastlane to v2.220.0 (#9491)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-14 14:12:27 -05:00
shenlong
6c008176c9 deps(mobile): update dependency auto_route to v8 (#9456)
deps: update dependency auto_route to v8

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-05-14 14:07:31 -05:00
renovate[bot]
72b1d582ba chore(deps): update dependency flutter_lints to v4 (#9488)
* chore(deps): update dependency flutter_lints to v4

* fix lints

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-05-14 18:52:07 +00:00
Jason Rasmussen
7b1112f3e3 refactor(server): system config (#9484) 2024-05-14 14:43:49 -04:00
renovate[bot]
42d0fc85ca chore(deps): update mobile (#9453)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-14 13:39:48 -05:00
Alex Tran
d32b621750 Merge branch 'main' of github.com:immich-app/immich 2024-05-14 13:38:15 -05:00
Alex Tran
d551003311 chore: post release tasks 2024-05-14 13:38:12 -05:00
dependabot[bot]
dd64379a4e chore(deps-dev): bump flask-cors from 4.0.0 to 4.0.1 in /machine-learning (#9485) 2024-05-14 14:06:40 -04:00
dependabot[bot]
c1cdda0ac4 chore(deps-dev): bump jinja2 from 3.1.3 to 3.1.4 in /machine-learning (#9483) 2024-05-14 17:08:29 +00:00
120 changed files with 1221 additions and 1076 deletions

1
base-images Submodule

Submodule base-images added at d0d3ab018c

2
cli/package-lock.json generated
View File

@@ -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": {

View File

@@ -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:

View File

@@ -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
View File

@@ -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": {

View File

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "1.105.0",
"version": "1.105.1",
"description": "",
"main": "index.js",
"type": "module",

View File

@@ -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

View File

@@ -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]

View File

@@ -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"

View File

@@ -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)

View File

@@ -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'

View File

@@ -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')

View File

@@ -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>

View File

@@ -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"

View File

@@ -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",

View File

@@ -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",

View File

@@ -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)

View File

@@ -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;

View File

@@ -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>

View File

@@ -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,

View File

@@ -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>

View File

@@ -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),

View File

@@ -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),
),
),

View File

@@ -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(

View File

@@ -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(

View File

@@ -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,

View File

@@ -23,7 +23,7 @@ class FailedBackupStatusPage extends HookConsumerWidget {
),
leading: IconButton(
onPressed: () {
context.popRoute(true);
context.maybePop(true);
},
splashRadius: 24,
icon: const Icon(

View File

@@ -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: [

View File

@@ -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()),

View File

@@ -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: [

View File

@@ -105,7 +105,7 @@ class AppLogPage extends HookConsumerWidget {
],
leading: IconButton(
onPressed: () {
context.popRoute();
context.maybePop();
},
icon: const Icon(
Icons.arrow_back_ios_new_rounded,

View File

@@ -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),
),

View File

@@ -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,

View File

@@ -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(),
};

View File

@@ -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,
),
),
],

View File

@@ -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,

View File

@@ -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,

View File

@@ -143,7 +143,7 @@ class TrashPage extends HookConsumerWidget {
return AppBar(
leading: IconButton(
onPressed: !selectionEnabledHook.value
? () => context.popRoute()
? () => context.maybePop()
: () {
selectionEnabledHook.value = false;
selection.value = {};

View File

@@ -176,7 +176,7 @@ class PermissionOnboardingPage extends HookConsumerWidget {
),
TextButton(
child: const Text('permission_onboarding_back').tr(),
onPressed: () => context.popRoute(),
onPressed: () => context.maybePop(),
),
],
),

View File

@@ -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,
);

View File

@@ -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),
),
),

View File

@@ -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),
),
),

View File

@@ -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),
),
),

View File

@@ -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),
),
),

View File

@@ -41,7 +41,7 @@ class MapLocationPickerPage extends HookConsumerWidget {
}
void onClose([LatLng? selected]) {
context.popRoute(selected);
context.maybePop(selected);
}
Future<void> getCurrentLocation() async {

View File

@@ -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: [

View File

@@ -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),
),
),

View File

@@ -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,

View File

@@ -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(

View File

@@ -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],

View File

@@ -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}';
}
}

View File

@@ -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),

View File

@@ -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,

View File

@@ -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,
);

View File

@@ -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();

View File

@@ -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,

View File

@@ -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(

View File

@@ -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(

View File

@@ -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(),
),

View File

@@ -43,7 +43,7 @@ class MemoryBottomInfo extends StatelessWidget {
MaterialButton(
minWidth: 0,
onPressed: () {
context.popRoute();
context.maybePop();
scrollToDateNotifierProvider
.scrollToDate(memory.assets[0].fileCreatedAt);
},

View File

@@ -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,
);
}
}

View File

@@ -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),
),
],
);
}
}

View File

@@ -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),
),
],
);
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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** | |

View File

@@ -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',

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -1,4 +1,5 @@
@Tags(['widget'])
library;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

View File

@@ -1,4 +1,5 @@
@Tags(['widget'])
library;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

View File

@@ -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",

View File

@@ -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"

View File

@@ -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",

View File

@@ -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;

View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "1.105.0",
"version": "1.105.1",
"description": "",
"author": "",
"private": true,

View File

@@ -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(),

View File

@@ -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';

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
};
}

View File

@@ -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';

View File

@@ -1,4 +1,4 @@
import { LogLevel } from 'src/entities/system-config.entity';
import { LogLevel } from 'src/config';
export const ILoggerRepository = 'ILoggerRepository';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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';

View File

@@ -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();
});
}
});
});

View File

@@ -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 } });
}

View File

@@ -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';

View File

@@ -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 () => {

View File

@@ -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