From f2b0b696f6e7c58814371e093e7fc4015e53c824 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 18 Jul 2026 12:07:18 -0500 Subject: [PATCH 001/204] fix: long press share quality override preference settings (#30030) --- .../share_action_button.widget.dart | 7 -- .../share_action_button_test.dart | 117 ++++++++++++++++++ 2 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 mobile/test/unit/presentation/action_buttons/share_action_button_test.dart diff --git a/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart index 6109b137a1..eef87f299d 100644 --- a/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart @@ -7,7 +7,6 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/settings_key.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; @@ -129,12 +128,6 @@ class ShareActionButton extends ConsumerWidget { return; } - await ref.read(settingsProvider).write(SettingsKey.shareFileType, fileType); - - if (!context.mounted) { - return; - } - await _share(context, ref, fileType); } diff --git a/mobile/test/unit/presentation/action_buttons/share_action_button_test.dart b/mobile/test/unit/presentation/action_buttons/share_action_button_test.dart new file mode 100644 index 0000000000..2f4aa3b8c9 --- /dev/null +++ b/mobile/test/unit/presentation/action_buttons/share_action_button_test.dart @@ -0,0 +1,117 @@ +import 'dart:async'; + +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/settings_key.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; + +import '../../factories/remote_asset_factory.dart'; +import '../presentation_context.dart'; + +class _RecordingActionNotifier extends ActionNotifier { + final List sharedFileTypes = []; + + @override + void build() {} + + @override + Future shareAssets( + ActionSource source, + BuildContext context, { + ShareAssetType fileType = ShareAssetType.original, + Completer? cancelCompleter, + void Function(double progress)? onAssetDownloadProgress, + }) async { + sharedFileTypes.add(fileType); + return const ActionResult(count: 1, success: true); + } +} + +class _FakeAssetViewerNotifier extends AssetViewerStateNotifier { + final BaseAsset asset; + + _FakeAssetViewerNotifier(this.asset); + + @override + AssetViewerState build() => AssetViewerState(currentAsset: asset); +} + +void main() { + late PresentationContext context; + late _RecordingActionNotifier actionNotifier; + + setUpAll(() async { + final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + await SettingsRepository.ensureInitialized(db); + }); + + setUp(() async { + context = await PresentationContext.create(); + actionNotifier = _RecordingActionNotifier(); + await SettingsRepository.instance.clear([SettingsKey.shareFileType]); + }); + + tearDown(() { + context.dispose(); + }); + + Future pumpShareButton(WidgetTester tester) async { + final asset = RemoteAssetFactory.create(ownerId: context.currentUser.id); + await tester.pumpTestWidget( + context, + const ShareActionButton(source: ActionSource.viewer), + overrides: [ + actionProvider.overrideWith(() => actionNotifier), + assetViewerProvider.overrideWith(() => _FakeAssetViewerNotifier(asset)), + ], + ); + } + + Future longPressAndPickPreview(WidgetTester tester) async { + await tester.longPress(find.byType(BaseActionButton)); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.photo_size_select_large_rounded)); + await tester.pumpAndSettle(); + } + + group('ShareActionButton', () { + testWidgets('single press shares with the configured default quality', (tester) async { + await pumpShareButton(tester); + + await tester.tap(find.byType(BaseActionButton)); + await tester.pumpAndSettle(); + + expect(actionNotifier.sharedFileTypes, [ShareAssetType.original]); + }); + + testWidgets('long press shares with the quality picked in the dialog', (tester) async { + await pumpShareButton(tester); + + await longPressAndPickPreview(tester); + + expect(actionNotifier.sharedFileTypes, [ShareAssetType.preview]); + }); + + testWidgets('quality picked on long press is a one-time choice and does not change the default', (tester) async { + await pumpShareButton(tester); + + await longPressAndPickPreview(tester); + expect(actionNotifier.sharedFileTypes, [ShareAssetType.preview]); + + await tester.tap(find.byType(BaseActionButton)); + await tester.pumpAndSettle(); + + expect(actionNotifier.sharedFileTypes, [ShareAssetType.preview, ShareAssetType.original]); + expect(SettingsRepository.instance.appConfig.share.fileType, ShareAssetType.original); + }); + }); +} From 00cb50cc67fe8872a552f5d844df48bac80f0bb7 Mon Sep 17 00:00:00 2001 From: Matthew Momjian <50788000+mmomjian@users.noreply.github.com> Date: Sun, 19 Jul 2026 22:22:17 -0400 Subject: [PATCH 002/204] fix(docs): remove ref to synology channel (#30051) synology channel --- docs/docs/install/synology.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/install/synology.md b/docs/docs/install/synology.md index de96886caa..69d8abf515 100644 --- a/docs/docs/install/synology.md +++ b/docs/docs/install/synology.md @@ -7,7 +7,7 @@ sidebar_position: 85 :::note This is a community contribution and not officially supported by the Immich team, but included here for convenience. -Community support can be found in the dedicated channel on the [Discord Server](https://discord.immich.app/). +Community support should be directed to Synology-specific support platforms. ::: Immich can easily be installed on a Synology NAS using Container Manager within DSM. If you have not installed Container Manager already, you can install it in the Packages Center. Refer to the [Container Manager docs](https://kb.synology.com/en-us/DSM/help/ContainerManager/docker_desc?version=7) for more information on using Container Manager. From 3adc3920fb6fd1a1b8b5254bc74116344c330226 Mon Sep 17 00:00:00 2001 From: Timon Date: Mon, 20 Jul 2026 15:06:51 +0200 Subject: [PATCH 003/204] chore: bump openapi-generator version to v7.23.0 (#28916) --- mobile/openapi/.openapi-generator/VERSION | 2 +- mobile/openapi/README.md | 2 +- mobile/openapi/lib/api_client.dart | 4 +- .../model/time_bucket_asset_response_dto.dart | 16 +- open-api/bin/generate-dart-sdk.sh | 3 +- open-api/openapitools.json | 2 +- open-api/patch/api_client.dart.patch | 80 +-------- .../time_bucket_asset_response_dto.dart.patch | 9 -- open-api/templates/mobile/api.mustache.patch | 27 +--- .../native/native_class.mustache.patch | 152 ------------------ 10 files changed, 24 insertions(+), 273 deletions(-) delete mode 100644 open-api/patch/time_bucket_asset_response_dto.dart.patch diff --git a/mobile/openapi/.openapi-generator/VERSION b/mobile/openapi/.openapi-generator/VERSION index 696eaac5ce..14d6b5dc35 100644 --- a/mobile/openapi/.openapi-generator/VERSION +++ b/mobile/openapi/.openapi-generator/VERSION @@ -1 +1 @@ -7.22.0 +7.23.0 diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index f3a601ae32..8fedc899a9 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -4,7 +4,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: - API version: 3.0.3 -- Generator version: 7.22.0 +- Generator version: 7.23.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen ## Requirements diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 873b05b8d2..4c6c1b5c72 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -97,9 +97,9 @@ class ApiClient { if (nullableHeaderParams != null) { request.headers.addAll(nullableHeaderParams); } - if (msgBody is String) { + if (msgBody is String && msgBody.isNotEmpty) { request.body = msgBody; - } else if (msgBody is List) { + } else if (msgBody is List && msgBody.isNotEmpty) { request.bodyBytes = msgBody; } else if (msgBody is Map) { request.bodyFields = msgBody; diff --git a/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart b/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart index 154ca97504..7662724070 100644 --- a/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart +++ b/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart @@ -35,16 +35,16 @@ class TimeBucketAssetResponseDto { }); /// Array of city names extracted from EXIF GPS data - Optional?> city; + Optional?> city; /// Array of country names extracted from EXIF GPS data - Optional?> country; + Optional?> country; /// Array of UTC timestamps when each asset was originally uploaded to Immich List createdAt; /// Array of video/gif durations in milliseconds (null for static images) - List duration; + List duration; /// Array of file creation timestamps in UTC List fileCreatedAt; @@ -62,22 +62,22 @@ class TimeBucketAssetResponseDto { List isTrashed; /// Array of latitude coordinates extracted from EXIF GPS data - Optional?> latitude; + Optional?> latitude; /// Array of live photo video asset IDs (null for non-live photos) - List livePhotoVideoId; + List livePhotoVideoId; /// Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective. List localOffsetHours; /// Array of longitude coordinates extracted from EXIF GPS data - Optional?> longitude; + Optional?> longitude; /// Array of owner IDs for each asset List ownerId; /// Array of projection types for 360° content (e.g., \"EQUIRECTANGULAR\", \"CUBEFACE\", \"CYLINDRICAL\") - List projectionType; + List projectionType; /// Array of aspect ratios (width/height) for each asset List ratio; @@ -86,7 +86,7 @@ class TimeBucketAssetResponseDto { Optional?>?> stack; /// Array of BlurHash strings for generating asset previews (base64 encoded) - List thumbhash; + List thumbhash; /// Array of visibility statuses for each asset (e.g., ARCHIVE, TIMELINE, HIDDEN, LOCKED) List visibility; diff --git a/open-api/bin/generate-dart-sdk.sh b/open-api/bin/generate-dart-sdk.sh index 793c1f8df3..fa2188a5bb 100755 --- a/open-api/bin/generate-dart-sdk.sh +++ b/open-api/bin/generate-dart-sdk.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -OPENAPI_GENERATOR_VERSION=v7.22.0 +OPENAPI_GENERATOR_VERSION=v7.23.0 set -euo pipefail @@ -23,7 +23,6 @@ patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api_client.dart <./patch/ patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api.dart <./patch/api.dart.patch patch --no-backup-if-mismatch -u ../mobile/openapi/pubspec.yaml <./patch/pubspec_immich_mobile.yaml.patch patch --no-backup-if-mismatch -u ../mobile/openapi/lib/model/asset_edit_action_item_dto.dart <./patch/asset_edit_action_item_dto.dart.patch -patch --no-backup-if-mismatch -u ../mobile/openapi/lib/model/time_bucket_asset_response_dto.dart <./patch/time_bucket_asset_response_dto.dart.patch # Don't include analysis_options.yaml for the generated openapi files # so that language servers can properly exclude the mobile/openapi directory rm ../mobile/openapi/analysis_options.yaml diff --git a/open-api/openapitools.json b/open-api/openapitools.json index 0f7e625af8..f3237adcbb 100644 --- a/open-api/openapitools.json +++ b/open-api/openapitools.json @@ -2,6 +2,6 @@ "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", "spaces": 2, "generator-cli": { - "version": "7.22.0" + "version": "7.23.0" } } diff --git a/open-api/patch/api_client.dart.patch b/open-api/patch/api_client.dart.patch index 55acb0d3cd..a813c1c033 100644 --- a/open-api/patch/api_client.dart.patch +++ b/open-api/patch/api_client.dart.patch @@ -1,96 +1,30 @@ @@ -13,7 +13,7 @@ class ApiClient { ApiClient({this.basePath = '/api', this.authentication,}); - + - final String basePath; + String basePath; final Authentication? authentication; - + var _client = Client(); -@@ -44,8 +44,9 @@ - Object? body, - Map headerParams, - Map formParams, -- String? contentType, -- ) async { -+ String? contentType, { -+ Future? abortTrigger, -+ }) async { - await authentication?.applyToParams(queryParams, headerParams); - - headerParams.addAll(_defaultHeaderMap); -@@ -63,7 +64,7 @@ - body is MultipartFile && (contentType == null || - !contentType.toLowerCase().startsWith('multipart/form-data')) - ) { -- final request = StreamedRequest(method, uri); -+ final request = AbortableStreamedRequest(method, uri, abortTrigger: abortTrigger); - request.headers.addAll(headerParams); - request.contentLength = body.length; - body.finalize().listen( -@@ -78,7 +79,7 @@ - } - - if (body is MultipartRequest) { -- final request = MultipartRequest(method, uri); -+ final request = AbortableMultipartRequest(method, uri, abortTrigger: abortTrigger); - request.fields.addAll(body.fields); - request.files.addAll(body.files); - request.headers.addAll(body.headers); -@@ -92,14 +93,19 @@ - : await serializeAsync(body); - final nullableHeaderParams = headerParams.isEmpty ? null : headerParams; - -- switch(method) { -- case 'POST': return await _client.post(uri, headers: nullableHeaderParams, body: msgBody,); -- case 'PUT': return await _client.put(uri, headers: nullableHeaderParams, body: msgBody,); -- case 'DELETE': return await _client.delete(uri, headers: nullableHeaderParams, body: msgBody,); -- case 'PATCH': return await _client.patch(uri, headers: nullableHeaderParams, body: msgBody,); -- case 'HEAD': return await _client.head(uri, headers: nullableHeaderParams,); -- case 'GET': return await _client.get(uri, headers: nullableHeaderParams,); -+ final request = AbortableRequest(method, uri, abortTrigger: abortTrigger); -+ if (nullableHeaderParams != null) { -+ request.headers.addAll(nullableHeaderParams); - } -+ if (msgBody is String) { -+ request.body = msgBody; -+ } else if (msgBody is List) { -+ request.bodyBytes = msgBody; -+ } else if (msgBody is Map) { -+ request.bodyFields = msgBody; -+ } -+ final response = await _client.send(request); -+ return Response.fromStream(response); - } on SocketException catch (error, trace) { - throw ApiException.withInner( - HttpStatus.badRequest, -@@ -136,26 +146,21 @@ - trace, - ); - } -- -- throw ApiException( -- HttpStatus.badRequest, -- 'Invalid HTTP operation: $method $path', -- ); +@@ -143,19 +143,19 @@ + ); } - + - Future deserializeAsync(String value, String targetType, {bool growable = false,}) async => + Future deserializeAsync(String value, String targetType, {bool growable = false,}) => // ignore: deprecated_member_use_from_same_package deserialize(value, targetType, growable: growable); - + @Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use deserializeAsync() instead.') - dynamic deserialize(String value, String targetType, {bool growable = false,}) { + Future deserialize(String value, String targetType, {bool growable = false,}) async { // Remove all spaces. Necessary for regular expressions as well. targetType = targetType.replaceAll(' ', ''); // ignore: parameter_assignments - + // If the expected target type is String, nothing to do... return targetType == 'String' ? value - : fromJson(json.decode(value), targetType, growable: growable); + : fromJson(await compute((String j) => json.decode(j), value), targetType, growable: growable); } - - // ignore: deprecated_member_use_from_same_package diff --git a/open-api/patch/time_bucket_asset_response_dto.dart.patch b/open-api/patch/time_bucket_asset_response_dto.dart.patch deleted file mode 100644 index 0ff420c4eb..0000000000 --- a/open-api/patch/time_bucket_asset_response_dto.dart.patch +++ /dev/null @@ -1,9 +0,0 @@ -@@ -83,7 +83,7 @@ - List ratio; - - /// Array of stack information as [stackId, assetCount] tuples (null for non-stacked assets) -- Optional>?> stack; -+ Optional?>?> stack; - - /// Array of BlurHash strings for generating asset previews (base64 encoded) - List thumbhash; diff --git a/open-api/templates/mobile/api.mustache.patch b/open-api/templates/mobile/api.mustache.patch index feb5f40047..8222ca4c2e 100644 --- a/open-api/templates/mobile/api.mustache.patch +++ b/open-api/templates/mobile/api.mustache.patch @@ -1,11 +1,8 @@ --- api.mustache +++ api.mustache.modified -@@ -49,9 +49,9 @@ - /// - {{/-last}} +@@ -51,7 +51,7 @@ {{/allParams}} -- Future {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} }{{/hasOptionalParams}}) async { -+ Future {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}}, {{/required}}{{/allParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}}, {{/required}}{{/allParams}}Future? abortTrigger, }) async { + Future {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} }{{/hasOptionalParams}}) async { // ignore: prefer_const_declarations - final path = r'{{{path}}}'{{#pathParams}} + final apiPath = r'{{{path}}}'{{#pathParams}} @@ -21,7 +18,7 @@ {{#formParams}} {{^isFile}} if ({{{paramName}}} != null) { -@@ -121,13 +121,14 @@ +@@ -121,7 +121,7 @@ {{/isMultipart}} return apiClient.invokeAPI( @@ -30,21 +27,3 @@ '{{{httpMethod}}}', queryParams, postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, -+ abortTrigger: abortTrigger, - ); - } - -@@ -161,8 +162,8 @@ - /// - {{/-last}} - {{/allParams}} -- Future<{{#returnType}}{{{.}}}?{{/returnType}}{{^returnType}}void{{/returnType}}> {{{nickname}}}({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} }{{/hasOptionalParams}}) async { -- final response = await {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}}{{#hasOptionalParams}} {{#allParams}}{{^required}}{{{paramName}}}: {{{paramName}}},{{^-last}} {{/-last}}{{/required}}{{/allParams}} {{/hasOptionalParams}}); -+ Future<{{#returnType}}{{{.}}}?{{/returnType}}{{^returnType}}void{{/returnType}}> {{{nickname}}}({{#allParams}}{{#required}}{{{dataType}}} {{{paramName}}}, {{/required}}{{/allParams}}{ {{#allParams}}{{^required}}{{{dataType}}}? {{{paramName}}}, {{/required}}{{/allParams}}Future? abortTrigger, }) async { -+ final response = await {{{nickname}}}WithHttpInfo({{#allParams}}{{#required}}{{{paramName}}}, {{/required}}{{/allParams}}{{#allParams}}{{^required}}{{{paramName}}}: {{{paramName}}}, {{/required}}{{/allParams}}abortTrigger: abortTrigger,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } diff --git a/open-api/templates/mobile/serialization/native/native_class.mustache.patch b/open-api/templates/mobile/serialization/native/native_class.mustache.patch index 6ab7a5228f..9894869a4a 100644 --- a/open-api/templates/mobile/serialization/native/native_class.mustache.patch +++ b/open-api/templates/mobile/serialization/native/native_class.mustache.patch @@ -26,155 +26,3 @@ return {{{classname}}}( {{#vars}} {{#isDateTime}} -@@ -195,48 +181,98 @@ - {{#complexType}} - {{#isArray}} - {{#items.isArray}} -+ {{#vendorExtensions.x-is-optional}} -+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(json[r'{{{baseName}}}'] is List -+ ? (json[r'{{{baseName}}}'] as List).map((e) => -+ {{#items.complexType}} -+ e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.complexType}}>[]{{/items.isNullable}} : {{items.complexType}}.listFromJson(e){{#uniqueItems}}.toSet(){{/uniqueItems}} -+ {{/items.complexType}} -+ {{^items.complexType}} -+ e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}>[]{{/items.isNullable}} : (e as List).map((value) => value as {{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}).toList(growable: false) -+ {{/items.complexType}} -+ ).toList() -+ : {{#isNullable}}null{{/isNullable}}{{^isNullable}}const []{{/isNullable}}) : const Optional.absent(), -+ {{/vendorExtensions.x-is-optional}} -+ {{^vendorExtensions.x-is-optional}} - {{{name}}}: json[r'{{{baseName}}}'] is List - ? (json[r'{{{baseName}}}'] as List).map((e) => - {{#items.complexType}} -- {{items.complexType}}.listFromJson(json[r'{{{baseName}}}']){{#uniqueItems}}.toSet(){{/uniqueItems}} -+ e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.complexType}}>[]{{/items.isNullable}} : {{items.complexType}}.listFromJson(e){{#uniqueItems}}.toSet(){{/uniqueItems}} - {{/items.complexType}} - {{^items.complexType}} -- e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}>[]{{/items.isNullable}} : (e as List).cast<{{items.items.dataType}}>() -+ e == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}>[]{{/items.isNullable}} : (e as List).map((value) => value as {{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}).toList(growable: false) - {{/items.complexType}} - ).toList() - : {{#isNullable}}null{{/isNullable}}{{^isNullable}}const []{{/isNullable}}, -+ {{/vendorExtensions.x-is-optional}} - {{/items.isArray}} - {{^items.isArray}} -+ {{#vendorExtensions.x-is-optional}} -+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present({{{complexType}}}.listFromJson(json[r'{{{baseName}}}']){{#uniqueItems}}.toSet(){{/uniqueItems}}) : const Optional.absent(), -+ {{/vendorExtensions.x-is-optional}} -+ {{^vendorExtensions.x-is-optional}} - {{{name}}}: {{{complexType}}}.listFromJson(json[r'{{{baseName}}}']){{#uniqueItems}}.toSet(){{/uniqueItems}}, -+ {{/vendorExtensions.x-is-optional}} - {{/items.isArray}} - {{/isArray}} - {{^isArray}} - {{#isMap}} - {{#items.isArray}} -+ {{#vendorExtensions.x-is-optional}} -+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(json[r'{{{baseName}}}'] == null ? null -+ {{#items.complexType}} -+ : {{items.complexType}}.mapListFromJson(json[r'{{{baseName}}}'])) : const Optional.absent(), -+ {{/items.complexType}} -+ {{^items.complexType}} -+ : (json[r'{{{baseName}}}'] as Map).map((k, v) => MapEntry(k, v == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}>[]{{/items.isNullable}} : (v as List).map((value) => value as {{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}).toList(growable: false)))) : const Optional.absent(), -+ {{/items.complexType}} -+ {{/vendorExtensions.x-is-optional}} -+ {{^vendorExtensions.x-is-optional}} - {{{name}}}: json[r'{{{baseName}}}'] == null - ? {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}} -- {{#items.complexType}} -+ {{#items.complexType}} - : {{items.complexType}}.mapListFromJson(json[r'{{{baseName}}}']), -- {{/items.complexType}} -- {{^items.complexType}} -- : (json[r'{{{baseName}}}'] as Map).map((k, v) => MapEntry(k, v == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}>[]{{/items.isNullable}} : (v as List).cast<{{items.items.dataType}}>())), -- {{/items.complexType}} -+ {{/items.complexType}} -+ {{^items.complexType}} -+ : (json[r'{{{baseName}}}'] as Map).map((k, v) => MapEntry(k, v == null ? {{#items.isNullable}}null{{/items.isNullable}}{{^items.isNullable}}const <{{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}>[]{{/items.isNullable}} : (v as List).map((value) => value as {{items.items.dataType}}{{#items.items.isNullable}}?{{/items.items.isNullable}}).toList(growable: false))), -+ {{/items.complexType}} -+ {{/vendorExtensions.x-is-optional}} - {{/items.isArray}} - {{^items.isArray}} - {{#items.isMap}} - {{#items.complexType}} -+ {{#vendorExtensions.x-is-optional}} -+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present({{items.complexType}}.mapFromJson(json[r'{{{baseName}}}'])) : const Optional.absent(), -+ {{/vendorExtensions.x-is-optional}} -+ {{^vendorExtensions.x-is-optional}} - {{{name}}}: {{items.complexType}}.mapFromJson(json[r'{{{baseName}}}']), -+ {{/vendorExtensions.x-is-optional}} - {{/items.complexType}} - {{^items.complexType}} -+ {{#vendorExtensions.x-is-optional}} -+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(mapCastOfType(json, r'{{{baseName}}}')) : const Optional.absent(), -+ {{/vendorExtensions.x-is-optional}} -+ {{^vendorExtensions.x-is-optional}} - {{{name}}}: mapCastOfType(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}, -+ {{/vendorExtensions.x-is-optional}} - {{/items.complexType}} - {{/items.isMap}} - {{^items.isMap}} - {{#items.complexType}} -+ {{#vendorExtensions.x-is-optional}} -+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present({{{items.complexType}}}.mapFromJson(json[r'{{{baseName}}}'])) : const Optional.absent(), -+ {{/vendorExtensions.x-is-optional}} -+ {{^vendorExtensions.x-is-optional}} - {{{name}}}: {{{items.complexType}}}.mapFromJson(json[r'{{{baseName}}}']), -+ {{/vendorExtensions.x-is-optional}} - {{/items.complexType}} - {{^items.complexType}} -+ {{#vendorExtensions.x-is-optional}} -+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(mapCastOfType(json, r'{{{baseName}}}')) : const Optional.absent(), -+ {{/vendorExtensions.x-is-optional}} -+ {{^vendorExtensions.x-is-optional}} - {{{name}}}: mapCastOfType(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}, -+ {{/vendorExtensions.x-is-optional}} - {{/items.complexType}} - {{/items.isMap}} - {{/items.isArray}} -@@ -259,23 +295,45 @@ - {{^complexType}} - {{#isArray}} - {{#isEnum}} -+ {{#vendorExtensions.x-is-optional}} -+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present({{{items.datatypeWithEnum}}}.listFromJson(json[r'{{{baseName}}}']){{#uniqueItems}}.toSet(){{/uniqueItems}}) : const Optional.absent(), -+ {{/vendorExtensions.x-is-optional}} -+ {{^vendorExtensions.x-is-optional}} - {{{name}}}: {{{items.datatypeWithEnum}}}.listFromJson(json[r'{{{baseName}}}']){{#uniqueItems}}.toSet(){{/uniqueItems}}, -+ {{/vendorExtensions.x-is-optional}} - {{/isEnum}} - {{^isEnum}} -+ {{#vendorExtensions.x-is-optional}} -+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(json[r'{{{baseName}}}'] is Iterable -+ ? (json[r'{{{baseName}}}'] as Iterable).cast<{{{items.datatype}}}>().{{#uniqueItems}}toSet(){{/uniqueItems}}{{^uniqueItems}}toList(growable: false){{/uniqueItems}} -+ : {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}}) : const Optional.absent(), -+ {{/vendorExtensions.x-is-optional}} -+ {{^vendorExtensions.x-is-optional}} - {{{name}}}: json[r'{{{baseName}}}'] is Iterable - ? (json[r'{{{baseName}}}'] as Iterable).cast<{{{items.datatype}}}>().{{#uniqueItems}}toSet(){{/uniqueItems}}{{^uniqueItems}}toList(growable: false){{/uniqueItems}} - : {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}}, -+ {{/vendorExtensions.x-is-optional}} - {{/isEnum}} - {{/isArray}} - {{^isArray}} - {{#isMap}} -+ {{#vendorExtensions.x-is-optional}} -+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(mapCastOfType(json, r'{{{baseName}}}')) : const Optional.absent(), -+ {{/vendorExtensions.x-is-optional}} -+ {{^vendorExtensions.x-is-optional}} - {{{name}}}: mapCastOfType(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}, -+ {{/vendorExtensions.x-is-optional}} - {{/isMap}} - {{^isMap}} - {{#isNumber}} -+ {{#vendorExtensions.x-is-optional}} -+ {{{name}}}: json.containsKey(r'{{{baseName}}}') ? Optional.present(json[r'{{{baseName}}}'] == null ? null : num.parse('${json[r'{{{baseName}}}']}')) : const Optional.absent(), -+ {{/vendorExtensions.x-is-optional}} -+ {{^vendorExtensions.x-is-optional}} - {{{name}}}: {{#isNullable}}json[r'{{{baseName}}}'] == null - ? {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}} - : {{/isNullable}}{{{datatypeWithEnum}}}.parse('${json[r'{{{baseName}}}']}'), -+ {{/vendorExtensions.x-is-optional}} - {{/isNumber}} - {{^isNumber}} - {{#vendorExtensions.x-original-is-integer}} From 522def1ed65541ef75cc58f2c6457cba9fd81ccb Mon Sep 17 00:00:00 2001 From: Priyanshu Date: Mon, 20 Jul 2026 19:24:59 +0530 Subject: [PATCH 004/204] fix(web): align ContextMenu z-index with design-system token (#30015) Co-authored-by: priyanshuANDcoad Co-authored-by: Daniel Dietzler --- .../shared-components/context-menu/ContextMenu.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/shared-components/context-menu/ContextMenu.svelte b/web/src/lib/components/shared-components/context-menu/ContextMenu.svelte index 729a4a4ea9..73f1406cc0 100644 --- a/web/src/lib/components/shared-components/context-menu/ContextMenu.svelte +++ b/web/src/lib/components/shared-components/context-menu/ContextMenu.svelte @@ -64,7 +64,7 @@
Date: Mon, 20 Jul 2026 21:50:52 +0530 Subject: [PATCH 005/204] fix(web): refresh folder view after asset deletion (#29899) --- .../folders/[[photos=photos]]/[[assetId=id]]/+page.svelte | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte index f44f80a243..8d6cbfeeb0 100644 --- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -1,6 +1,7 @@ diff --git a/web/src/lib/components/asset-viewer/editor/transform-tool/TransformTool.svelte b/web/src/lib/components/asset-viewer/editor/transform-tool/TransformTool.svelte index 29e4e09506..5bc50f9431 100644 --- a/web/src/lib/components/asset-viewer/editor/transform-tool/TransformTool.svelte +++ b/web/src/lib/components/asset-viewer/editor/transform-tool/TransformTool.svelte @@ -37,9 +37,8 @@ if (isRotated) { let [width, height] = ratio.value.split(':'); return `${height}:${width}`; - } else { - return ratio.value; } + return ratio.value; } function ratioSelected(ratio: AspectRatioOption): boolean { diff --git a/web/src/lib/components/asset-viewer/immich-time-range.ts b/web/src/lib/components/asset-viewer/immich-time-range.ts index a3de131e73..12d6200d51 100644 --- a/web/src/lib/components/asset-viewer/immich-time-range.ts +++ b/web/src/lib/components/asset-viewer/immich-time-range.ts @@ -49,6 +49,6 @@ class ImmichTimeRange extends MediaTimeRange { } } -if (!globalThis.customElements.get('immich-time-range')) { - globalThis.customElements.define('immich-time-range', ImmichTimeRange); +if (!customElements.get('immich-time-range')) { + customElements.define('immich-time-range', ImmichTimeRange); } diff --git a/web/src/lib/components/assets/thumbnail/Thumbnail.svelte b/web/src/lib/components/assets/thumbnail/Thumbnail.svelte index 6e73952b18..5764965bdc 100644 --- a/web/src/lib/components/assets/thumbnail/Thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/Thumbnail.svelte @@ -167,7 +167,7 @@ e.preventDefault(); }; element.addEventListener('click', click); - element.addEventListener('pointerdown', start, true); + element.addEventListener('pointerdown', start, { capture: true }); element.addEventListener('pointerup', clearLongPressTimer, { capture: true, passive: true }); return { destroy: () => { @@ -215,8 +215,7 @@ onkeydown={(evt) => { if (evt.key === 'Enter') { callClickHandlers(); - } - if (evt.key === 'x') { + } else if (evt.key === 'x') { onSelect?.(asset); } if (document.activeElement === element && evt.key === 'Escape') { diff --git a/web/src/lib/components/assets/thumbnail/VideoThumbnail.svelte b/web/src/lib/components/assets/thumbnail/VideoThumbnail.svelte index c62abac3c2..2875d96a9c 100644 --- a/web/src/lib/components/assets/thumbnail/VideoThumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/VideoThumbnail.svelte @@ -85,10 +85,7 @@ }} ontimeupdate={({ currentTarget }) => { const remaining = currentTarget.duration - currentTarget.currentTime; - remainingSeconds = Math.min( - Math.ceil(Number.isNaN(remaining) ? Number.POSITIVE_INFINITY : remaining), - durationInSeconds, - ); + remainingSeconds = Math.min(Math.ceil(Number.isNaN(remaining) ? Infinity : remaining), durationInSeconds); }} > {/if} diff --git a/web/src/lib/components/faces-page/PersonSidePanel.svelte b/web/src/lib/components/faces-page/PersonSidePanel.svelte index eedd6766eb..eaeabcc921 100644 --- a/web/src/lib/components/faces-page/PersonSidePanel.svelte +++ b/web/src/lib/components/faces-page/PersonSidePanel.svelte @@ -93,10 +93,10 @@ }; const handleReset = (id: string) => { - if (selectedPersonToReassign[id]) { + if (Object.hasOwn(selectedPersonToReassign, id)) { delete selectedPersonToReassign[id]; } - if (selectedPersonToCreate[id]) { + if (Object.hasOwn(selectedPersonToCreate, id)) { delete selectedPersonToCreate[id]; } }; @@ -115,7 +115,7 @@ id: personId, faceDto: { id: personWithFace.id }, }); - } else if (selectedPersonToCreate[personWithFace.id]) { + } else if (Object.hasOwn(selectedPersonToCreate, personWithFace.id)) { const data = await createPerson({ personCreateDto: {} }); peopleToCreate.push(data.id); await reassignFacesById({ @@ -314,7 +314,7 @@ {/if}
- {#if !selectedPersonToCreate[face.id]} + {#if !Object.hasOwn(selectedPersonToCreate, face.id)}

{#if selectedPersonToReassign[face.id]?.id} {selectedPersonToReassign[face.id]?.name} @@ -349,7 +349,7 @@ {/if}

- {#if !selectedPersonToCreate[face.id] && !selectedPersonToReassign[face.id] && !face.person} + {#if !Object.hasOwn(selectedPersonToCreate, face.id) && !Object.hasOwn(selectedPersonToReassign, face.id) && !face.person}
diff --git a/web/src/lib/components/maintenance/MaintenanceBackupsList.svelte b/web/src/lib/components/maintenance/MaintenanceBackupsList.svelte index 8582806c94..b3b21a2f3e 100644 --- a/web/src/lib/components/maintenance/MaintenanceBackupsList.svelte +++ b/web/src/lib/components/maintenance/MaintenanceBackupsList.svelte @@ -71,7 +71,7 @@ } // Sort by date descending (newest first), but put unknown date at the top - const sortedEntries = [...groups.entries()].sort((a, b) => { + const sortedEntries = [...groups].sort((a, b) => { if (a[0] === unknownDateKey) { return -1; } @@ -115,7 +115,7 @@
- {#each [...groupedBackups.entries()] as [dateGroup, groupBackups] (dateGroup)} + {#each [...groupedBackups] as [dateGroup, groupBackups] (dateGroup)}
diff --git a/web/src/lib/components/server-statistics/ServerStatisticsCard.svelte b/web/src/lib/components/server-statistics/ServerStatisticsCard.svelte index ab2e6ce4f4..a9fb831e82 100644 --- a/web/src/lib/components/server-statistics/ServerStatisticsCard.svelte +++ b/web/src/lib/components/server-statistics/ServerStatisticsCard.svelte @@ -21,7 +21,7 @@ let length = 13; if (data) { const valueLength = data.value.toString().length; - length = length - valueLength; + length -= valueLength; } return '0'.repeat(length); diff --git a/web/src/lib/components/share-page/IndividualSharedViewer.svelte b/web/src/lib/components/share-page/IndividualSharedViewer.svelte index a3cf76c78e..a119f4ffea 100644 --- a/web/src/lib/components/share-page/IndividualSharedViewer.svelte +++ b/web/src/lib/components/share-page/IndividualSharedViewer.svelte @@ -35,10 +35,12 @@ let assets = $derived(sharedLink.assets); dragAndDropFilesStore.subscribe((value) => { - if (value.isDragging && value.files.length > 0) { - handlePromiseError(handleUploadAssets(value.files)); - dragAndDropFilesStore.set({ isDragging: false, files: [] }); + if (!(value.isDragging && value.files.length > 0)) { + return; } + + handlePromiseError(handleUploadAssets(value.files)); + dragAndDropFilesStore.set({ isDragging: false, files: [] }); }); const downloadAssets = async () => { diff --git a/web/src/lib/components/shared-components/Combobox.svelte b/web/src/lib/components/shared-components/Combobox.svelte index 7b0e867343..388317a4ef 100644 --- a/web/src/lib/components/shared-components/Combobox.svelte +++ b/web/src/lib/components/shared-components/Combobox.svelte @@ -345,10 +345,12 @@ { shortcut: { key: 'Escape' }, onShortcut: (event) => { - if (isOpen) { - event.stopPropagation(); - closeDropdown(); + if (!isOpen) { + return; } + + event.stopPropagation(); + closeDropdown(); }, }, ]} @@ -399,7 +401,7 @@ aria-selected={selectedIndex === 0} aria-disabled={true} class="w-full cursor-default px-4 py-2 text-start hover:bg-gray-200 aria-selected:bg-gray-200 dark:hover:bg-gray-700 aria-selected:dark:bg-gray-700" - id={`${listboxId}-${0}`} + id={`${listboxId}-0`} onclick={closeDropdown} > {allowCreate ? searchQuery : $t('no_results')} diff --git a/web/src/lib/components/shared-components/SingleGridRow.svelte b/web/src/lib/components/shared-components/SingleGridRow.svelte index c4db34c152..b71086830c 100644 --- a/web/src/lib/components/shared-components/SingleGridRow.svelte +++ b/web/src/lib/components/shared-components/SingleGridRow.svelte @@ -18,7 +18,7 @@ }; }; - const parsePixels = (style: string) => Number.parseInt(style, 10) || 0; + const parsePixels = (style: string) => Math.trunc(Number(style)) || 0; const getItemCount = (container: HTMLElement, containerWidth: number) => { if (!container.firstElementChild) { diff --git a/web/src/lib/components/shared-components/context-menu/MenuOption.svelte b/web/src/lib/components/shared-components/context-menu/MenuOption.svelte index b4a6f060a0..fe39b36c8e 100644 --- a/web/src/lib/components/shared-components/context-menu/MenuOption.svelte +++ b/web/src/lib/components/shared-components/context-menu/MenuOption.svelte @@ -32,6 +32,7 @@ let isActive = $derived($selectedIdStore === id); const handleClick = () => { + // eslint-disable-next-line unicorn/no-optional-chaining-on-undeclared-variable $optionClickCallbackStore?.(); onClick(); }; diff --git a/web/src/lib/components/shared-components/context-menu/RightClickContextMenu.svelte b/web/src/lib/components/shared-components/context-menu/RightClickContextMenu.svelte index 6b685f6d2d..16b1df1716 100644 --- a/web/src/lib/components/shared-components/context-menu/RightClickContextMenu.svelte +++ b/web/src/lib/components/shared-components/context-menu/RightClickContextMenu.svelte @@ -57,11 +57,13 @@ onClose?.(); }; $effect(() => { - if (isOpen && menuContainer) { - triggerElement = document.activeElement as HTMLElement; - menuContainer.focus(); - $optionClickCallbackStore = closeContextMenu; + if (!(isOpen && menuContainer)) { + return; } + + triggerElement = document.activeElement as HTMLElement; + menuContainer.focus(); + $optionClickCallbackStore = closeContextMenu; }); const oncontextmenu = async (event: MouseEvent) => { diff --git a/web/src/lib/components/shared-components/gallery-viewer/GalleryViewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/GalleryViewer.svelte index 29e7e6458c..5a4b25225f 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/GalleryViewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/GalleryViewer.svelte @@ -109,12 +109,14 @@ let lastEndReachedHeight = 0; $effect(() => { - if (geometry.containerHeight - slidingWindow.bottom <= viewport.height) { - const contentHeight = geometry.containerHeight; - if (lastEndReachedHeight !== contentHeight) { - debouncedOnEndReached(); - lastEndReachedHeight = contentHeight; - } + if (geometry.containerHeight - slidingWindow.bottom > viewport.height) { + return; + } + + const contentHeight = geometry.containerHeight; + if (lastEndReachedHeight !== contentHeight) { + debouncedOnEndReached(); + lastEndReachedHeight = contentHeight; } }); diff --git a/web/src/lib/components/shared-components/map/Map.svelte b/web/src/lib/components/shared-components/map/Map.svelte index ade37d0322..52805d9b96 100644 --- a/web/src/lib/components/shared-components/map/Map.svelte +++ b/web/src/lib/components/shared-components/map/Map.svelte @@ -109,14 +109,16 @@ ); export function addClipMapMarker(lng: number, lat: number) { - if (map) { - if (marker) { - marker.remove(); - } - - center = { lng, lat }; - marker = new Marker().setLngLat([lng, lat]).addTo(map); + if (!map) { + return; } + + if (marker) { + marker.remove(); + } + + center = { lng, lat }; + marker = new Marker().setLngLat([lng, lat]).addTo(map); } function handleAssetClick(assetId: string, map: Map | null) { @@ -159,17 +161,19 @@ } function handleMapClick(event: MapMouseEvent) { - if (clickable) { - const { lng, lat } = event.lngLat; - onClickPoint({ lng, lat }); + if (!clickable) { + return; + } - if (marker) { - marker.remove(); - } + const { lng, lat } = event.lngLat; + onClickPoint({ lng, lat }); - if (map) { - marker = new Marker().setLngLat([lng, lat]).addTo(map); - } + if (marker) { + marker.remove(); + } + + if (map) { + marker = new Marker().setLngLat([lng, lat]).addTo(map); } } @@ -254,13 +258,16 @@ }; afterNavigate(() => { - if (map) { - map.resize(); + if (!map) { + return; + } - if (globalThis.location.hash) { - const hashChangeEvent = new HashChangeEvent('hashchange'); - globalThis.dispatchEvent(hashChangeEvent); - } + map.resize(); + + if (location.hash) { + const hashChangeEvent = new HashChangeEvent('hashchange'); + // eslint-disable-next-line unicorn/no-unnecessary-global-this + globalThis.dispatchEvent(hashChangeEvent); } }); diff --git a/web/src/lib/components/shared-components/search-bar/SearchBar.svelte b/web/src/lib/components/shared-components/search-bar/SearchBar.svelte index 339bfb64a2..b099020e77 100644 --- a/web/src/lib/components/shared-components/search-bar/SearchBar.svelte +++ b/web/src/lib/components/shared-components/search-bar/SearchBar.svelte @@ -156,10 +156,12 @@ }; const onEnter = (event: KeyboardEvent) => { - if (selectedId) { - event.preventDefault(); - searchHistoryBox?.selectActiveOption(); + if (!selectedId) { + return; } + + event.preventDefault(); + searchHistoryBox?.selectActiveOption(); }; const onInput = () => { diff --git a/web/src/lib/components/shared-components/search-bar/SearchHistoryBox.svelte b/web/src/lib/components/shared-components/search-bar/SearchHistoryBox.svelte index 24ceaeb380..a1e3d9328f 100644 --- a/web/src/lib/components/shared-components/search-bar/SearchHistoryBox.svelte +++ b/web/src/lib/components/shared-components/search-bar/SearchHistoryBox.svelte @@ -44,7 +44,8 @@ export function moveSelection(increment: 1 | -1) { if (!isSearchSuggestions) { return; - } else if (selectedIndex === undefined) { + } + if (selectedIndex === undefined) { selectedIndex = increment === 1 ? 0 : suggestionCount - 1; } else if (selectedIndex + increment < 0 || selectedIndex + increment >= suggestionCount) { clearSelection(); diff --git a/web/src/lib/components/shared-components/settings/SettingsLanguageSelector.svelte b/web/src/lib/components/shared-components/settings/SettingsLanguageSelector.svelte index 146c37b7ec..ca1e3059c3 100644 --- a/web/src/lib/components/shared-components/settings/SettingsLanguageSelector.svelte +++ b/web/src/lib/components/shared-components/settings/SettingsLanguageSelector.svelte @@ -18,11 +18,13 @@ const defaultLangOption = { label: defaultLang.name, value: defaultLang.code }; const handleLanguageChange = async (newLang: string | undefined) => { - if (newLang) { - $lang = newLang; - await i18nLocale.set(convertBCP47(newLang)); - await invalidateAll(); + if (!newLang) { + return; } + + $lang = newLang; + await i18nLocale.set(convertBCP47(newLang)); + await invalidateAll(); }; let closestLanguage = $derived(getClosestAvailableLocale([$lang], langCodes)); diff --git a/web/src/lib/components/shared-components/side-bar/RecentAlbums.svelte b/web/src/lib/components/shared-components/side-bar/RecentAlbums.svelte index 2d4be43a00..10c772d8b8 100644 --- a/web/src/lib/components/shared-components/side-bar/RecentAlbums.svelte +++ b/web/src/lib/components/shared-components/side-bar/RecentAlbums.svelte @@ -11,7 +11,7 @@ const refreshAlbums = async () => { try { const allAlbums = await getAllAlbums({}); - albums = allAlbums.sort((a, b) => (a.updatedAt > b.updatedAt ? -1 : 1)).slice(0, 3); + albums = allAlbums.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()).slice(0, 3); userInteraction.recentAlbums = albums; } catch (error) { handleError(error, $t('failed_to_load_assets')); diff --git a/web/src/lib/components/timeline/Scrubber.svelte b/web/src/lib/components/timeline/Scrubber.svelte index 03a3e43d8e..67ec87ff7d 100644 --- a/web/src/lib/components/timeline/Scrubber.svelte +++ b/web/src/lib/components/timeline/Scrubber.svelte @@ -99,13 +99,15 @@ ) => { if (scrubberMonth === 'lead-in') { return relativeTopOffset * scrubberMonthPercent; - } else if (scrubberMonth === 'lead-out') { + } + if (scrubberMonth === 'lead-out') { let offset = relativeTopOffset; for (const segment of segments) { offset += segment.height; } return offset + relativeBottomOffset * scrubberMonthPercent; - } else if (scrubberMonth) { + } + if (scrubberMonth) { let offset = relativeTopOffset; let match = false; for (const segment of segments) { @@ -120,9 +122,8 @@ offset += scrubberMonthPercent * relativeBottomOffset; } return offset; - } else { - return scrubOverallPercent * (height - (PADDING_TOP + PADDING_BOTTOM)); } + return scrubOverallPercent * (height - (PADDING_TOP + PADDING_BOTTOM)); }; const scrollY = $derived( toScrollFromTimelineMonthPercentage(viewportTopMonth, viewportTopMonthScrollPercent, timelineScrollPercent), @@ -229,14 +230,14 @@ if (scrollY !== undefined) { if (scrollY < relativeTopOffset) { return segments.at(0)?.dateFormatted; - } else { - let offset = relativeTopOffset; - for (const segment of segments) { - offset += segment.height; - } - if (scrollY > offset) { - return segments.at(-1)?.dateFormatted; - } + } + + let offset = relativeTopOffset; + for (const segment of segments) { + offset += segment.height; + } + if (scrollY > offset) { + return segments.at(-1)?.dateFormatted; } } return scrollSegment?.dateFormatted || ''; @@ -338,7 +339,7 @@ overallScrollPercent: toTimelineY(hoverY), scrubberMonthScrollPercent: timelineMonthPercentY, }; - if (wasDragging === false && isDragging) { + if (!wasDragging && isDragging) { void startScrub?.(scrubData); void onScrub?.(scrubData); } diff --git a/web/src/lib/components/timeline/Timeline.svelte b/web/src/lib/components/timeline/Timeline.svelte index 7949e2ff1b..1fe6c7b097 100644 --- a/web/src/lib/components/timeline/Timeline.svelte +++ b/web/src/lib/components/timeline/Timeline.svelte @@ -201,7 +201,7 @@ export const scrollAfterNavigate = async () => { if (timelineManager.viewportHeight === 0 || timelineManager.viewportWidth === 0) { // this can happen if you do the following navigation order - // /photos?at=, /photos/, http://example.com, browser back, browser back + // /photos?at=, /photos/, https://example.com, browser back, browser back const rect = scrollableElement?.getBoundingClientRect(); if (rect) { timelineManager.viewportHeight = rect.height; @@ -209,10 +209,7 @@ } } const scrollTarget = assetViewerManager.gridScrollTarget?.at; - let scrolled = false; - if (scrollTarget) { - scrolled = await scrollAndLoadAsset(scrollTarget); - } + const scrolled = scrollTarget ? await scrollAndLoadAsset(scrollTarget) : false; if (!scrolled) { // if the asset is not found, scroll to the top timelineManager.scrollTo(0); @@ -503,10 +500,12 @@ }); $effect(() => { - if (assetViewerManager.asset && assetViewerManager.isViewing) { - const { localDateTime } = getTimes(assetViewerManager.asset.fileCreatedAt, DateTime.local().offset / 60); - void timelineManager.loadTimelineMonth({ year: localDateTime.year, month: localDateTime.month }); + if (!(assetViewerManager.asset && assetViewerManager.isViewing)) { + return; } + + const { localDateTime } = getTimes(assetViewerManager.asset.fileCreatedAt, DateTime.local().offset / 60); + void timelineManager.loadTimelineMonth({ year: localDateTime.year, month: localDateTime.month }); }); const assetSelectHandler = ( @@ -582,10 +581,7 @@ bind:scrubberWidth onScrubKeyDown={(evt) => { evt.preventDefault(); - let amount = 50; - if (keyboardManager.shift) { - amount = 500; - } + let amount = keyboardManager.shift ? 500 : 50; if (evt.key === 'ArrowUp') { amount = -amount; if (keyboardManager.shift) { diff --git a/web/src/lib/elements/FormatMessage.svelte b/web/src/lib/elements/FormatMessage.svelte index 3385ab3b56..53f72451cc 100644 --- a/web/src/lib/elements/FormatMessage.svelte +++ b/web/src/lib/elements/FormatMessage.svelte @@ -41,10 +41,12 @@ for (const option of Object.values(element.options)) { for (const pluralElement of option.value) { - if (pluralElement.type === TYPE.tag) { - const tag = pluralElement.value; - replacements[tag] = (...parts) => `<${tag}>${parts}`; + if (pluralElement.type !== TYPE.tag) { + continue; } + + const tag = pluralElement.value; + replacements[tag] = (...parts) => `<${tag}>${parts}`; } } diff --git a/web/src/lib/managers/asset-viewer-manager.svelte.ts b/web/src/lib/managers/asset-viewer-manager.svelte.ts index 551f50bc6d..94a6d00700 100644 --- a/web/src/lib/managers/asset-viewer-manager.svelte.ts +++ b/web/src/lib/managers/asset-viewer-manager.svelte.ts @@ -134,10 +134,12 @@ class AssetViewerManager extends BaseEventManager { } cancelZoomAnimation() { - if (this.#animationFrameId !== null) { - cancelAnimationFrame(this.#animationFrameId); - this.#animationFrameId = null; + if (this.#animationFrameId === null) { + return; } + + cancelAnimationFrame(this.#animationFrameId); + this.#animationFrameId = null; } animatedZoom(targetZoom: number, duration = 300) { diff --git a/web/src/lib/managers/auth-manager.svelte.ts b/web/src/lib/managers/auth-manager.svelte.ts index 38f71811ae..4b1743b9d2 100644 --- a/web/src/lib/managers/auth-manager.svelte.ts +++ b/web/src/lib/managers/auth-manager.svelte.ts @@ -109,7 +109,7 @@ class AuthManager { await goto(redirectUri); } else { - globalThis.location.href = redirectUri; + location.assign(redirectUri); } } diff --git a/web/src/lib/managers/download-manager.svelte.ts b/web/src/lib/managers/download-manager.svelte.ts index 107f80b8dc..f08e5eb929 100644 --- a/web/src/lib/managers/download-manager.svelte.ts +++ b/web/src/lib/managers/download-manager.svelte.ts @@ -16,7 +16,7 @@ class DownloadManager { return; } - if (!this.assets[key]) { + if (!Object.hasOwn(this.assets, key)) { this.assets[key] = { progress: 0, total: 0, percentage: 0, abort: null }; } diff --git a/web/src/lib/managers/edit/transform-manager.svelte.ts b/web/src/lib/managers/edit/transform-manager.svelte.ts index 8826d58d11..9379138a1b 100644 --- a/web/src/lib/managers/edit/transform-manager.svelte.ts +++ b/web/src/lib/managers/edit/transform-manager.svelte.ts @@ -193,6 +193,7 @@ class TransformManager implements EditToolManager { passive: true, }); + // eslint-disable-next-line unicorn/no-unnecessary-global-this globalThis.addEventListener('mousemove', (e: MouseEvent) => transformManager.handleMouseMove(e), { passive: true }); const transformEdits = edits.filter((e) => e.action === 'rotate' || e.action === 'mirror'); @@ -210,6 +211,7 @@ class TransformManager implements EditToolManager { } onDeactivate() { + // eslint-disable-next-line unicorn/no-unnecessary-global-this globalThis.removeEventListener('mousemove', transformManager.handleMouseMove); this.reset(); @@ -553,6 +555,7 @@ class TransformManager implements EditToolManager { } document.body.style.userSelect = 'none'; + // eslint-disable-next-line unicorn/no-unnecessary-global-this globalThis.addEventListener('mouseup', () => this.handleMouseUp(), { passive: true }); } @@ -571,6 +574,7 @@ class TransformManager implements EditToolManager { } handleMouseUp() { + // eslint-disable-next-line unicorn/no-unnecessary-global-this globalThis.removeEventListener('mouseup', this.handleMouseUp); document.body.style.userSelect = ''; diff --git a/web/src/lib/managers/language-manager.svelte.ts b/web/src/lib/managers/language-manager.svelte.ts index c690197e1f..cf5352a8d2 100644 --- a/web/src/lib/managers/language-manager.svelte.ts +++ b/web/src/lib/managers/language-manager.svelte.ts @@ -13,10 +13,11 @@ class LanguageManager { rtl = $state(false); init() { - if (!this.initialized) { - this.initialized = true; - lang.subscribe((lang) => this.setLanguage(lang)); + if (this.initialized) { + return; } + this.initialized = true; + lang.subscribe((lang) => this.setLanguage(lang)); } setLanguage(code: string) { diff --git a/web/src/lib/managers/media-capabilities-manager.svelte.ts b/web/src/lib/managers/media-capabilities-manager.svelte.ts index ccabc6680d..96e2a783cc 100644 --- a/web/src/lib/managers/media-capabilities-manager.svelte.ts +++ b/web/src/lib/managers/media-capabilities-manager.svelte.ts @@ -89,4 +89,5 @@ class MediaCapabilitiesManager { } export const mediaCapabilitiesManager = new MediaCapabilitiesManager(); +// eslint-disable-next-line unicorn/no-top-level-side-effects mediaCapabilitiesManager.init(); diff --git a/web/src/lib/managers/timeline-manager/group-insertion-cache.svelte.ts b/web/src/lib/managers/timeline-manager/group-insertion-cache.svelte.ts index 5c373c0437..a363a05959 100644 --- a/web/src/lib/managers/timeline-manager/group-insertion-cache.svelte.ts +++ b/web/src/lib/managers/timeline-manager/group-insertion-cache.svelte.ts @@ -19,10 +19,10 @@ export class GroupInsertionCache { } setTimelineDay(timelineDay: TimelineDay, { year, month, day }: TimelineDate) { - if (!this.#lookupCache[year]) { + if (!Object.hasOwn(this.#lookupCache, year)) { this.#lookupCache[year] = {}; } - if (!this.#lookupCache[year][month]) { + if (!Object.hasOwn(this.#lookupCache[year], month)) { this.#lookupCache[year][month] = {}; } this.#lookupCache[year][month][day] = timelineDay; diff --git a/web/src/lib/managers/timeline-manager/internal/layout-support.svelte.ts b/web/src/lib/managers/timeline-manager/internal/layout-support.svelte.ts index fc2902bb63..3f9854161f 100644 --- a/web/src/lib/managers/timeline-manager/internal/layout-support.svelte.ts +++ b/web/src/lib/managers/timeline-manager/internal/layout-support.svelte.ts @@ -41,8 +41,6 @@ export function layoutTimelineMonth(timelineManager: TimelineManager, month: Tim timelineDay.col = timelineDayCol++; timelineDay.start = cumulativeWidth; timelineDay.top = cumulativeHeight; - - cumulativeWidth += timelineDay.width + timelineManager.gap; } else { // Move to next row cumulativeHeight += currentRowHeight; @@ -57,8 +55,8 @@ export function layoutTimelineMonth(timelineManager: TimelineManager, month: Tim timelineDay.top = cumulativeHeight; timelineDayCol++; - cumulativeWidth += timelineDay.width + timelineManager.gap; } + cumulativeWidth += timelineDay.width + timelineManager.gap; currentRowHeight = timelineDay.height + timelineManager.headerHeight; } diff --git a/web/src/lib/managers/timeline-manager/internal/utils.svelte.ts b/web/src/lib/managers/timeline-manager/internal/utils.svelte.ts index a1b580a966..0dcf7346f6 100644 --- a/web/src/lib/managers/timeline-manager/internal/utils.svelte.ts +++ b/web/src/lib/managers/timeline-manager/internal/utils.svelte.ts @@ -13,7 +13,7 @@ export function updateObject(target: any, source: any): boolean { } const isDate = target[key] instanceof Date; if (typeof target[key] === 'object' && !isDate) { - updated = updated || updateObject(target[key], source[key]); + updated ||= updateObject(target[key], source[key]); } else { if (target[key] !== source[key]) { target[key] = source[key]; diff --git a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts index 6defe1d498..0b9c6d4b07 100644 --- a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts +++ b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts @@ -357,10 +357,7 @@ export class TimelineManager extends VirtualScrollManager { } async loadTimelineMonth(yearMonth: TimelineYearMonth, options?: { cancelable: boolean }): Promise { - let cancelable = true; - if (options) { - cancelable = options.cancelable; - } + const cancelable = options?.cancelable ?? true; const timelineMonth = getTimelineMonthByDate(this, yearMonth); if (!timelineMonth) { return; @@ -517,10 +514,7 @@ export class TimelineManager extends VirtualScrollManager { // eslint-disable-next-line svelte/prefer-svelte-reactivity const idsToUpdate = new Set(cache.keys()); const result = this.#runAssetCallback(idsToUpdate, (asset) => void updateObject(asset, cache.get(asset.id))); - const notUpdated: TimelineAsset[] = []; - for (const assetId of result.notUpdated) { - notUpdated.push(cache.get(assetId)!); - } + const notUpdated: TimelineAsset[] = Array.from(result.notUpdated, (assetId) => cache.get(assetId)!); return notUpdated; } diff --git a/web/src/lib/managers/timeline-manager/timeline-month.svelte.ts b/web/src/lib/managers/timeline-manager/timeline-month.svelte.ts index 30896a97a4..58b36769b9 100644 --- a/web/src/lib/managers/timeline-manager/timeline-month.svelte.ts +++ b/web/src/lib/managers/timeline-manager/timeline-month.svelte.ts @@ -145,21 +145,23 @@ export class TimelineMonth { const combinedMoveAssets: MoveAsset[][] = []; let index = timelineDays.length; while (index--) { - if (idsToProcess.size > 0) { - const group = timelineDays[index]; - const { moveAssets, processedIds, changedGeometry } = group.runAssetCallback(ids, callback); - if (moveAssets.length > 0) { - combinedMoveAssets.push(moveAssets); - } - idsToProcess = setDifference(idsToProcess, processedIds); - for (const id of processedIds) { - idsProcessed.add(id); - } - combinedChangedGeometry = combinedChangedGeometry || changedGeometry; - if (group.viewerAssets.length === 0) { - timelineDays.splice(index, 1); - combinedChangedGeometry = true; - } + if (idsToProcess.size === 0) { + continue; + } + + const group = timelineDays[index]; + const { moveAssets, processedIds, changedGeometry } = group.runAssetCallback(ids, callback); + if (moveAssets.length > 0) { + combinedMoveAssets.push(moveAssets); + } + idsToProcess = setDifference(idsToProcess, processedIds); + for (const id of processedIds) { + idsProcessed.add(id); + } + combinedChangedGeometry ||= changedGeometry; + if (group.viewerAssets.length === 0) { + timelineDays.splice(index, 1); + combinedChangedGeometry = true; } } return { @@ -195,7 +197,7 @@ export class TimelineMonth { ownerId: bucketAssets.ownerId[i], projectionType: bucketAssets.projectionType[i], ratio: bucketAssets.ratio[i], - stack: bucketAssets.stack?.[i] + stack: bucketAssets.stack?.at(i) ? { id: bucketAssets.stack[i]![0], primaryAssetId: bucketAssets.id[i], @@ -206,7 +208,7 @@ export class TimelineMonth { people: null, // People are not included in the bucket assets }; - if (bucketAssets.latitude?.[i] && bucketAssets.longitude?.[i]) { + if (bucketAssets.latitude?.at(i) && bucketAssets.longitude?.at(i)) { timelineAsset.latitude = bucketAssets.latitude?.[i]; timelineAsset.longitude = bucketAssets.longitude?.[i]; } diff --git a/web/src/lib/modals/AlbumPickerModal.svelte b/web/src/lib/modals/AlbumPickerModal.svelte index 1d96a04eff..561deeac23 100644 --- a/web/src/lib/modals/AlbumPickerModal.svelte +++ b/web/src/lib/modals/AlbumPickerModal.svelte @@ -137,6 +137,7 @@ break; } case 'Control': { + // eslint-disable-next-line unicorn/no-late-event-control e.preventDefault(); handleMultiSelect(); break; diff --git a/web/src/lib/modals/GeolocationPointPickerModal.svelte b/web/src/lib/modals/GeolocationPointPickerModal.svelte index 069f62d3ac..657f919433 100644 --- a/web/src/lib/modals/GeolocationPointPickerModal.svelte +++ b/web/src/lib/modals/GeolocationPointPickerModal.svelte @@ -85,8 +85,8 @@ // Try to parse coordinate pair from search input in the format `LATITUDE, LONGITUDE` as floats const coordinateParts = searchWord.split(',').map((part) => part.trim()); if (coordinateParts.length === 2) { - const coordinateLat = Number.parseFloat(coordinateParts[0]); - const coordinateLng = Number.parseFloat(coordinateParts[1]); + const coordinateLat = Number(coordinateParts[0]); + const coordinateLng = Number(coordinateParts[1]); if ( !Number.isNaN(coordinateLat) && @@ -106,18 +106,22 @@ searchPlaces({ name: searchWord }) .then((searchResult) => { // skip result when a newer search is happening - if (latestSearchTimeout === searchTimeout) { - places = searchResult; - showLoadingSpinner = false; + if (latestSearchTimeout !== searchTimeout) { + return; } + + places = searchResult; + showLoadingSpinner = false; }) .catch((error) => { // skip error when a newer search is happening - if (latestSearchTimeout === searchTimeout) { - places = []; - handleError(error, $t('errors.cant_search_places')); - showLoadingSpinner = false; + if (latestSearchTimeout !== searchTimeout) { + return; } + + places = []; + handleError(error, $t('errors.cant_search_places')); + showLoadingSpinner = false; }); }, timeDebounceOnSearch); latestSearchTimeout = searchTimeout; diff --git a/web/src/lib/modals/PersonMergeSuggestionModal.svelte b/web/src/lib/modals/PersonMergeSuggestionModal.svelte index 1471bc4a51..c449ed35cf 100644 --- a/web/src/lib/modals/PersonMergeSuggestionModal.svelte +++ b/web/src/lib/modals/PersonMergeSuggestionModal.svelte @@ -28,6 +28,7 @@ const changePersonToMerge = (newPerson: PersonResponseDto) => { const index = potentialMergePeople.indexOf(newPerson); + // eslint-disable-next-line unicorn/no-unreadable-array-destructuring [potentialMergePeople[index], personToBeMergedInto] = [personToBeMergedInto, potentialMergePeople[index]]; choosePersonToMerge = false; }; diff --git a/web/src/lib/modals/SearchFilterModal.svelte b/web/src/lib/modals/SearchFilterModal.svelte index 1eb88f67b6..058cdc8e6f 100644 --- a/web/src/lib/modals/SearchFilterModal.svelte +++ b/web/src/lib/modals/SearchFilterModal.svelte @@ -45,10 +45,8 @@ } const asFilter = (searchQuery: SmartSearchDto | MetadataSearchDto): SearchFilter => { - let query = ''; - if ('query' in searchQuery && searchQuery.query) { - query = searchQuery.query; - } + let query = 'query' in searchQuery && searchQuery.query ? searchQuery.query : ''; + if ('originalFileName' in searchQuery && searchQuery.originalFileName) { query = searchQuery.originalFileName; } diff --git a/web/src/lib/modals/ServerAboutModal.svelte b/web/src/lib/modals/ServerAboutModal.svelte index 412dbe4e20..6fc3ff99bc 100644 --- a/web/src/lib/modals/ServerAboutModal.svelte +++ b/web/src/lib/modals/ServerAboutModal.svelte @@ -61,7 +61,7 @@ {/if} - {#if info.buildImage && info.buildImage} + {#if info.buildImage && info.buildImageUrl} { describe(Route.continue.name, () => { beforeEach(() => { // @ts-expect-error - override location for testing + // eslint-disable-next-line unicorn/no-global-object-property-assignment globalThis.location = new URL('https://my.immich.server'); vi.spyOn(document, 'baseURI', 'get').mockReturnValue('https://my.immich.server/'); }); diff --git a/web/src/lib/route.ts b/web/src/lib/route.ts index 49e51928f6..d47340a34b 100644 --- a/web/src/lib/route.ts +++ b/web/src/lib/route.ts @@ -31,11 +31,7 @@ const asQueryString = ( return false; } - if (skipEmptyStrings && value === '') { - return false; - } - - return true; + return !(skipEmptyStrings && value === ''); }) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`); diff --git a/web/src/lib/services/database-backups.service.ts b/web/src/lib/services/database-backups.service.ts index 4f2b97d72e..6a379ce287 100644 --- a/web/src/lib/services/database-backups.service.ts +++ b/web/src/lib/services/database-backups.service.ts @@ -91,7 +91,7 @@ export const handleDeleteDatabaseBackup = async (...filenames: string[]) => { }; export const handleDownloadDatabaseBackup = (filename: string) => { - location.href = getBaseUrl() + '/admin/database-backups/' + filename; + location.assign(getBaseUrl() + '/admin/database-backups/' + filename); }; export const handleUploadDatabaseBackup = async () => { diff --git a/web/src/lib/services/shared-link.service.ts b/web/src/lib/services/shared-link.service.ts index 3699c0ed75..885caee7d8 100644 --- a/web/src/lib/services/shared-link.service.ts +++ b/web/src/lib/services/shared-link.service.ts @@ -61,7 +61,7 @@ export const getSharedLinkActions = ($t: MessageFormatter, sharedLink: SharedLin export const asUrl = (sharedLink: SharedLinkResponseDto) => { const path = Route.viewSharedLink(sharedLink); - return new URL(path, serverConfigManager.value.externalDomain || globalThis.location.origin).href; + return new URL(path, serverConfigManager.value.externalDomain || location.origin).href; }; export const handleCreateSharedLink = async (dto: SharedLinkCreateDto) => { diff --git a/web/src/lib/services/system-config.service.ts b/web/src/lib/services/system-config.service.ts index cc7c961d1e..b8d7a0f1a7 100644 --- a/web/src/lib/services/system-config.service.ts +++ b/web/src/lib/services/system-config.service.ts @@ -64,7 +64,7 @@ export const handleSystemConfigSave = async (update: Partial) = }; export const handleUploadConfig = () => { - const input = globalThis.document.createElement('input'); + const input = document.createElement('input'); input.setAttribute('type', 'file'); input.setAttribute('accept', '.json'); input.setAttribute('style', 'display: none'); @@ -83,6 +83,6 @@ export const handleUploadConfig = () => { .catch((error) => console.error('Error handling JSON config upload', error)) .finally(() => input.remove()); }); - globalThis.document.body.append(input); + document.body.append(input); input.click(); }; diff --git a/web/src/lib/services/user-admin.service.ts b/web/src/lib/services/user-admin.service.ts index f386ba9c86..51b39b9d22 100644 --- a/web/src/lib/services/user-admin.service.ts +++ b/web/src/lib/services/user-admin.service.ts @@ -159,11 +159,11 @@ export const handleNavigateUserAdmin = async (user: UserAdminResponseDto) => { const generatePassword = (length: number = 16) => { let generatedPassword = ''; - const characterSet = '0123456789' + 'abcdefghijklmnopqrstuvwxyz' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + ',.-{}+!#$%/()=?'; + const characterSet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,.-{}+!#$%/()=?'; for (let i = 0; i < length; i++) { let randomNumber = crypto.getRandomValues(new Uint32Array(1))[0]; - randomNumber = randomNumber / 2 ** 32; + randomNumber /= 2 ** 32; randomNumber = Math.floor(randomNumber * characterSet.length); generatedPassword += characterSet[randomNumber]; diff --git a/web/src/lib/services/workflow.service.ts b/web/src/lib/services/workflow.service.ts index c81b1b2706..1fba144604 100644 --- a/web/src/lib/services/workflow.service.ts +++ b/web/src/lib/services/workflow.service.ts @@ -3,8 +3,6 @@ import { deleteWorkflow, updateWorkflow, WorkflowTrigger, - type AlbumResponseDto, - type PersonResponseDto, type WorkflowCreateDto, type WorkflowResponseDto, type WorkflowUpdateDto, @@ -32,9 +30,6 @@ import { copyToClipboard, downloadJson } from '$lib/utils'; import { handleError } from '$lib/utils/handle-error'; import { getFormatter } from '$lib/utils/i18n'; -export type PickerSubType = 'album-picker' | 'people-picker'; -export type PickerMetadata = AlbumResponseDto | PersonResponseDto | AlbumResponseDto[] | PersonResponseDto[]; - export const getWorkflowsActions = ($t: MessageFormatter) => { const Create: ActionItem = { title: $t('create_workflow'), diff --git a/web/src/lib/stores/keyboard-manager.svelte.ts b/web/src/lib/stores/keyboard-manager.svelte.ts index e2ed5bae8c..774acc6874 100644 --- a/web/src/lib/stores/keyboard-manager.svelte.ts +++ b/web/src/lib/stores/keyboard-manager.svelte.ts @@ -8,8 +8,11 @@ class KeyboardManager { if (globalThis.window === undefined) { return; } + // eslint-disable-next-line unicorn/no-unnecessary-global-this globalThis.addEventListener('keydown', this.#update); + // eslint-disable-next-line unicorn/no-unnecessary-global-this globalThis.addEventListener('keyup', this.#update); + // eslint-disable-next-line unicorn/no-unnecessary-global-this globalThis.addEventListener('blur', this.#clear); } diff --git a/web/src/lib/stores/slideshow.store.ts b/web/src/lib/stores/slideshow.store.ts index a576d8f37b..17ba266ee1 100644 --- a/web/src/lib/stores/slideshow.store.ts +++ b/web/src/lib/stores/slideshow.store.ts @@ -58,10 +58,12 @@ function createSlideshowStore() { set: (value: boolean) => { // Trigger an action whenever the restartProgress is set to true. Automatically // reset the restart state after that - if (value) { - restartState.set(true); - restartState.set(false); + if (!value) { + return; } + + restartState.set(true); + restartState.set(false); }, }, stopProgress: { @@ -69,10 +71,12 @@ function createSlideshowStore() { set: (value: boolean) => { // Trigger an action whenever the stopProgress is set to true. Automatically // reset the stop state after that - if (value) { - stopState.set(true); - stopState.set(false); + if (!value) { + return; } + + stopState.set(true); + stopState.set(false); }, }, slideshowNavigation, diff --git a/web/src/lib/stores/upload.ts b/web/src/lib/stores/upload.ts index bcbf7792e8..04a8a45bee 100644 --- a/web/src/lib/stores/upload.ts +++ b/web/src/lib/stores/upload.ts @@ -23,7 +23,7 @@ function createUploadStore() { const addItem = (newAsset: UploadAsset) => { uploadAssets.update(($assets) => { - const duplicate = $assets.find((asset) => asset.id === newAsset.id); + const duplicate = $assets.some((asset) => asset.id === newAsset.id); if (duplicate) { return $assets.map((asset) => (asset.id === newAsset.id ? newAsset : asset)); } diff --git a/web/src/lib/stores/user.svelte.ts b/web/src/lib/stores/user.svelte.ts index 98454e4249..e5bb4dfa27 100644 --- a/web/src/lib/stores/user.svelte.ts +++ b/web/src/lib/stores/user.svelte.ts @@ -30,6 +30,7 @@ const reset = () => { Object.assign(userInteraction, defaultUserInteraction); }; +// eslint-disable-next-line unicorn/no-top-level-side-effects eventManager.on({ AlbumCreate: () => resetRecentAlbums(), AlbumUpdate: () => resetRecentAlbums(), diff --git a/web/src/lib/stores/websocket.ts b/web/src/lib/stores/websocket.ts index fc33812973..6c4ac1cfd6 100644 --- a/web/src/lib/stores/websocket.ts +++ b/web/src/lib/stores/websocket.ts @@ -60,6 +60,7 @@ export const websocketStore = { export const websocketEvents = createEventEmitter(websocket); +// eslint-disable-next-line unicorn/no-top-level-side-effects websocket .on('connect', () => { eventManager.emit('WebsocketConnect'); @@ -113,16 +114,18 @@ export const waitForWebsocketEvent = ( return new Promise((resolve, reject) => { // @ts-expect-error: The typings are weird on this? const cleanup = websocketEvents.on(event, (...args: Parameters) => { - if (!predicate || predicate(...args)) { - cleanup(); - clearTimeout(timer); - resolve(args); + if (predicate && !predicate(...args)) { + return; } + + cleanup(); + clearTimeout(timer); + resolve(args); }); const timer = setTimeout(() => { cleanup(); - reject(new Error(`Timeout waiting for event: ${String(event)}`)); + reject(new Error(`Timeout waiting for event: ${event}`)); }, timeout); }); }; diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index 0e31782164..3aecb5df59 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -109,11 +109,10 @@ export const uploadRequest = async (options: UploadRequestOptions): Promise<{ }); xhr.addEventListener('load', () => { + unsubscribe(); if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 300) { - unsubscribe(); resolve({ data: xhr.response as T, status: xhr.status }); } else { - unsubscribe(); reject(new ApiError(xhr.statusText, xhr.status, xhr.response)); } }); @@ -326,9 +325,9 @@ export const oauth = { authorize: async (location: Location) => { const $t = get(t); try { - const redirectUri = location.href.split('?')[0]; + const redirectUri = location.href.split('?', 1)[0]; const { url } = await startOAuth({ oAuthConfigDto: { redirectUri } }); - globalThis.location.href = url; + globalThis.location.assign(url); return true; } catch (error) { handleError(error, $t('errors.unable_to_login_with_oauth')); @@ -430,7 +429,8 @@ export const isEnabled = ({ $if }: IfLike) => $if?.() ?? true; export const transformToTitleCase = (text: string) => { if (text.length === 0) { return text; - } else if (text.length === 1) { + } + if (text.length === 1) { return text.charAt(0).toUpperCase(); } diff --git a/web/src/lib/utils/actions.ts b/web/src/lib/utils/actions.ts index 59c431557b..3624b34f93 100644 --- a/web/src/lib/utils/actions.ts +++ b/web/src/lib/utils/actions.ts @@ -68,19 +68,21 @@ const undoDeleteAssets = async (onUndoDelete: OnUndoDelete, assets: TimelineAsse * @param {StackResponse} stackResponse - The stack response containing the stack and assets to delete. */ export function updateStackedAssetInTimeline(timelineManager: TimelineManager, { stack, toDeleteIds }: StackResponse) { - if (stack != undefined) { - timelineManager.update( - [stack.primaryAssetId], - (asset) => - (asset.stack = { - id: stack.id, - primaryAssetId: stack.primaryAssetId, - assetCount: stack.assets.length, - }), - ); - - timelineManager.removeAssets(toDeleteIds); + if (stack == undefined) { + return; } + + timelineManager.update( + [stack.primaryAssetId], + (asset) => + (asset.stack = { + id: stack.id, + primaryAssetId: stack.primaryAssetId, + assetCount: stack.assets.length, + }), + ); + + timelineManager.removeAssets(toDeleteIds); } /** diff --git a/web/src/lib/utils/adaptive-image-loader.svelte.ts b/web/src/lib/utils/adaptive-image-loader.svelte.ts index 8d9a5f79f4..c82d17ec98 100644 --- a/web/src/lib/utils/adaptive-image-loader.svelte.ts +++ b/web/src/lib/utils/adaptive-image-loader.svelte.ts @@ -86,6 +86,7 @@ export class AdaptiveImageLoader { const config = this.qualityConfigs[quality]; + // eslint-disable-next-line unicorn/no-computed-property-existence-check if (!this.status.urls[quality]) { return; } @@ -129,6 +130,7 @@ export class AdaptiveImageLoader { return false; } + // eslint-disable-next-line unicorn/no-computed-property-existence-check if (this.status.urls[quality]) { return true; } diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index fdfc15e636..5b8de3cfb0 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -92,13 +92,13 @@ export const downloadArchive = async (fileName: string, options: Omit 1 ? `+${index + 1}` : ''; - const archiveName = fileName.replace('.zip', `${suffix}-${DateTime.now().toFormat('yyyyLLdd_HHmmss')}.zip`); + const archiveName = fileName.replace('.zip', () => `${suffix}-${DateTime.now().toFormat('yyyyLLdd_HHmmss')}.zip`); const queryParams = asQueryString(authManager.params); - let downloadKey = `${archiveName} `; - if (downloadInfo.archives.length > 1) { - downloadKey = `${archiveName} (${index + 1}/${downloadInfo.archives.length})`; - } + const downloadKey = + downloadInfo.archives.length > 1 + ? `${archiveName} (${index + 1}/${downloadInfo.archives.length})` + : `${archiveName} `; const abort = new AbortController(); downloadManager.add(downloadKey, archive.size, abort); @@ -131,7 +131,7 @@ export const downloadArchive = async (fileName: string, options: Omit { heicImg.src = 'data:image/heic;base64,AAAAGGZ0eXBoZWljAAAAAG1pZjFoZWljAAABrW1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAHBpY3QAAAAAAAAAAAAAAAAAAAAADnBpdG0AAAAAAAIAAAAQaWRhdAAAAAAAAQABAAAAOGlsb2MBAAAAREAAAgABAAAAAAAAAc0AAQAAAAAAAAAsAAIAAQAAAAAAAAABAAAAAAAAAAgAAAA4aWluZgAAAAAAAgAAABVpbmZlAgAAAQABAABodmMxAAAAABVpbmZlAgAAAAACAABncmlkAAAAANhpcHJwAAAAtmlwY28AAAB2aHZjQwEDcAAAAAAAAAAAAB7wAPz9+PgAAA8DIAABABhAAQwB//8DcAAAAwCQAAADAAADAB66AkAhAAEAKkIBAQNwAAADAJAAAAMAAAMAHqAggQWW6q6a5uBAQMCAAAADAIAAAAMAhCIAAQAGRAHBc8GJAAAAFGlzcGUAAAAAAAAAAQAAAAEAAAAUaXNwZQAAAAAAAABAAAAAQAAAABBwaXhpAAAAAAMICAgAAAAaaXBtYQAAAAAAAAACAAECgQMAAgIChAAAABppcmVmAAAAAAAAAA5kaW1nAAIAAQABAAAANG1kYXQAAAAoKAGvCchMZYA50NoPIfzz81Qfsm577GJt3lf8kLAr+NbNIoeRR7JeYA=='; // Small valid HEIC/HEIF image } +// eslint-disable-next-line unicorn/no-top-level-side-effects void addSupportedMimeTypes(); /** diff --git a/web/src/lib/utils/auth.ts b/web/src/lib/utils/auth.ts index 13a50f33ec..aa6ffccdbf 100644 --- a/web/src/lib/utils/auth.ts +++ b/web/src/lib/utils/auth.ts @@ -28,10 +28,12 @@ export const authenticate = async (url: URL, options?: AuthOptions) => { }; export const requestServerInfo = async () => { - if (authManager.authenticated) { - const data = await getStorage(); - userInteraction.serverInfo = data; + if (!authManager.authenticated) { + return; } + + const data = await getStorage(); + userInteraction.serverInfo = data; }; export const getAccountAge = (): number => { diff --git a/web/src/lib/utils/byte-units.ts b/web/src/lib/utils/byte-units.ts index 7a5576d7f5..e903c7829e 100644 --- a/web/src/lib/utils/byte-units.ts +++ b/web/src/lib/utils/byte-units.ts @@ -23,7 +23,7 @@ const byteUnits = [ByteUnit.B, ByteUnit.KiB, ByteUnit.MiB, ByteUnit.GiB, ByteUni export function getBytesWithUnit(bytes: number, maxPrecision = 1): [number, ByteUnit] { const magnitude = Math.floor(Math.log(bytes === 0 ? 1 : bytes) / Math.log(1024)); - return [Number.parseFloat((bytes / 1024 ** magnitude).toFixed(maxPrecision)), byteUnits[magnitude]]; + return [Number((bytes / 1024 ** magnitude).toFixed(maxPrecision)), byteUnits[magnitude]]; } /** diff --git a/web/src/lib/utils/cast/gcast-destination.svelte.ts b/web/src/lib/utils/cast/gcast-destination.svelte.ts index f2a51c5c6a..71766366e1 100644 --- a/web/src/lib/utils/cast/gcast-destination.svelte.ts +++ b/web/src/lib/utils/cast/gcast-destination.svelte.ts @@ -41,11 +41,12 @@ export class GCastDestination implements ICastDestination { return; } + // eslint-disable-next-line unicorn/no-global-object-property-assignment window['__onGCastApiAvailable'] = (isAvailable: boolean) => { resolve(isAvailable); }; - if (!document.querySelector(`script[src="${FRAMEWORK_LINK}"]`)) { + if (!document.querySelector(`script[src="${CSS.escape(FRAMEWORK_LINK)}"]`)) { const script = document.createElement('script'); script.src = FRAMEWORK_LINK; document.body.append(script); diff --git a/web/src/lib/utils/date-time.ts b/web/src/lib/utils/date-time.ts index 17704aff22..7cfeadb9dc 100644 --- a/web/src/lib/utils/date-time.ts +++ b/web/src/lib/utils/date-time.ts @@ -27,23 +27,23 @@ export const getShortDateRange = (startTimestamp: string, endTimestamp: string) // Same year and month. // e.g.: aug. 2024 return endDateLocalized; - } else { - // Same year but different month. - // e.g.: jul. - sept. 2024 - const startMonthLocalized = startDate.toLocaleString({ - month: 'short', - }); - return `${startMonthLocalized} - ${endDateLocalized}`; } - } else { - // Different year. - // e.g.: feb. 2021 - sept. 2024 - const startDateLocalized = startDate.toLocaleString({ + + // Same year but different month. + // e.g.: jul. - sept. 2024 + const startMonthLocalized = startDate.toLocaleString({ month: 'short', - year: 'numeric', }); - return `${startDateLocalized} - ${endDateLocalized}`; + return `${startMonthLocalized} - ${endDateLocalized}`; } + + // Different year. + // e.g.: feb. 2021 - sept. 2024 + const startDateLocalized = startDate.toLocaleString({ + month: 'short', + year: 'numeric', + }); + return `${startDateLocalized} - ${endDateLocalized}`; }; const formatDate = (date?: string) => { diff --git a/web/src/lib/utils/duplicate-utils.ts b/web/src/lib/utils/duplicate-utils.ts index dac4e9dc54..86b221906f 100644 --- a/web/src/lib/utils/duplicate-utils.ts +++ b/web/src/lib/utils/duplicate-utils.ts @@ -209,7 +209,7 @@ const metadataFields = [ icon: mdiPhoneRotateLandscape, titleKey: 'orientation', keys: ['orientation'], - render: (asset, $t) => String(asset.exifInfo?.orientation || $t('unknown')), + render: (asset, $t) => asset.exifInfo?.orientation || $t('unknown'), }, { icon: mdiPanorama, @@ -240,7 +240,7 @@ const normalizeForComparison = (key: MetadataFieldKey, value: unknown): unknown return value; } - if (key === 'fileCreatedAt' || key === 'fileModifiedAt' || key === 'dateTimeOriginal' || key === 'modifyDate') { + if (['fileCreatedAt', 'fileModifiedAt', 'dateTimeOriginal', 'modifyDate'].includes(key)) { const dateTime = DateTime.fromISO(String(value)); return dateTime.isValid ? dateTime.toISO() : String(value); } @@ -273,7 +273,7 @@ const getValueForAsset = (asset: AssetResponseDto, key: MetadataFieldKey): unkno return getAssetResolution(asset); } default: { - if (asset.exifInfo && key in asset.exifInfo) { + if (asset.exifInfo && Object.hasOwn(asset.exifInfo, key)) { return asset.exifInfo[key as keyof typeof asset.exifInfo]; } return undefined; diff --git a/web/src/lib/utils/file-uploader.ts b/web/src/lib/utils/file-uploader.ts index 0d0630a017..10ed7b08d3 100644 --- a/web/src/lib/utils/file-uploader.ts +++ b/web/src/lib/utils/file-uploader.ts @@ -131,7 +131,7 @@ export const fileUploadHandler = async ({ }; function getDeviceAssetId(asset: File) { - return 'web' + '-' + asset.name + '-' + asset.lastModified; + return 'web-' + asset.name + '-' + asset.lastModified; } function hashFile(file: File): Promise { diff --git a/web/src/lib/utils/handle-error.ts b/web/src/lib/utils/handle-error.ts index 6a1634b711..fe1f7a6a0c 100644 --- a/web/src/lib/utils/handle-error.ts +++ b/web/src/lib/utils/handle-error.ts @@ -47,7 +47,7 @@ export function handleError(error: unknown, localizedMessage: string, options?: try { let serverMessage = getServerErrorMessage(error); if (serverMessage) { - serverMessage = `${String(serverMessage).slice(0, 75)}\n(Immich Server Error)`; + serverMessage = `${serverMessage.slice(0, 75)}\n(Immich Server Error)`; } const errorMessage = serverMessage || localizedMessage; diff --git a/web/src/lib/utils/invocationTracker.ts b/web/src/lib/utils/invocationTracker.ts index 88c395a4ad..ada3b12d0e 100644 --- a/web/src/lib/utils/invocationTracker.ts +++ b/web/src/lib/utils/invocationTracker.ts @@ -30,10 +30,7 @@ export class InvocationTracker { * @throws {Error} If the invocation is no longer valid */ isStillValid: () => { - if (invocation !== this.invocationsStarted) { - return false; - } - return true; + return invocation === this.invocationsStarted; }, /** diff --git a/web/src/lib/utils/navigation.ts b/web/src/lib/utils/navigation.ts index cfeb22f560..91513911f1 100644 --- a/web/src/lib/utils/navigation.ts +++ b/web/src/lib/utils/navigation.ts @@ -8,7 +8,7 @@ export type AssetGridRouteSearchParams = { at: string | null | undefined; }; export const isExternalUrl = (url: string): boolean => { - return new URL(url, globalThis.location.href).origin !== globalThis.location.origin; + return new URL(url, location.href).origin !== location.origin; }; export const isPhotosRoute = (route?: string | null) => !!route?.startsWith('/(user)/photos/[[assetId=id]]'); @@ -33,11 +33,10 @@ function currentUrlWithoutAsset() { // off / instead of a subpath, unlike every other asset-containing route. if (isPhotosRoute(page.route.id)) { return Route.photos() + page.url.search; - } else if (isSharedLinkSlugRoute(page.route.id)) { - return Route.viewSharedLink({ slug: page.data.slug, key: page.data.key }) + page.url.search; - } else { - return page.url.pathname.replace(/(\/photos.*)$/, '') + page.url.search; } + return isSharedLinkSlugRoute(page.route.id) + ? Route.viewSharedLink({ slug: page.data.slug, key: page.data.key }) + page.url.search + : page.url.pathname.replace(/(\/photos.*)$/, '') + page.url.search; } export function currentUrlReplaceAssetId(assetId: string) { @@ -133,7 +132,8 @@ async function navigateAssetGridRoute(route: AssetGridRoute, options?: NavOption export function navigate(change: ImmichRoute, options?: NavOptions): Promise { if (isAssetGridRoute(change)) { return navigateAssetGridRoute(change, options); - } else if (isAssetRoute(change)) { + } + if (isAssetRoute(change)) { return navigateAssetRoute(change, options); } // future navigation requests here @@ -141,20 +141,22 @@ export function navigate(change: ImmichRoute, options?: NavOptions): Promise { - if (url.searchParams.has(queryParam)) { - url.searchParams.delete(queryParam); - await goto(url, { keepFocus: true }); + if (!url.searchParams.has(queryParam)) { + return; } + + url.searchParams.delete(queryParam); + await goto(url, { keepFocus: true }); }; export const getQueryValue = (queryKey: string) => { - const url = globalThis.location.href; + const url = location.href; const urlObject = new URL(url); return urlObject.searchParams.get(queryKey); }; export const setQueryValue = async (queryKey: string, queryValue: string) => { - const url = globalThis.location.href; + const url = location.href; const urlObject = new URL(url); urlObject.searchParams.set(queryKey, queryValue); await goto(urlObject, { keepFocus: true }); diff --git a/web/src/lib/utils/ocr-utils.ts b/web/src/lib/utils/ocr-utils.ts index 997027179b..e21cfb3fb2 100644 --- a/web/src/lib/utils/ocr-utils.ts +++ b/web/src/lib/utils/ocr-utils.ts @@ -17,7 +17,7 @@ export type OcrBox = { }; const CJK_PATTERN = - /[\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\uAC00-\uD7AF\uFF00-\uFFEF]/; + /[\u{3000}-\u{303F}\u{3040}-\u{309F}\u{30A0}-\u{30FF}\u{3400}-\u{4DBF}\u{4E00}-\u{9FFF}\u{F900}-\u{FAFF}\u{AC00}-\u{D7AF}\u{FF00}-\u{FFEF}]/u; const VERTICAL_ASPECT_RATIO = 1.5; diff --git a/web/src/lib/utils/string-utils.ts b/web/src/lib/utils/string-utils.ts index 0170c34737..d92ad0a923 100644 --- a/web/src/lib/utils/string-utils.ts +++ b/web/src/lib/utils/string-utils.ts @@ -1,5 +1,5 @@ export const removeAccents = (str: string) => { - return str.normalize('NFD').replaceAll(/[\u0300-\u036F]/g, ''); + return str.normalize('NFD').replaceAll(/[\u{300}-\u{36F}]/gu, ''); }; export const normalizeSearchString = (str: string) => { diff --git a/web/src/lib/utils/timeline-util.ts b/web/src/lib/utils/timeline-util.ts index a54a9a6e26..018d7d34ef 100644 --- a/web/src/lib/utils/timeline-util.ts +++ b/web/src/lib/utils/timeline-util.ts @@ -95,8 +95,8 @@ export const fromTimelinePlainYearMonth = (timelineYearMonth: TimelineYearMonth) ) as DateTime; export const toISOYearMonthUTC = ({ year, month }: TimelineYearMonth): string => { - const yearFull = `${year}`.padStart(4, '0'); - const monthFull = `${month}`.padStart(2, '0'); + const yearFull = String(year).padStart(4, '0'); + const monthFull = String(month).padStart(2, '0'); return `${yearFull}-${monthFull}-01T00:00:00.000Z`; }; diff --git a/web/src/lib/utils/wakelock.svelte.ts b/web/src/lib/utils/wakelock.svelte.ts index 4c69a397a5..78e0ee1daa 100644 --- a/web/src/lib/utils/wakelock.svelte.ts +++ b/web/src/lib/utils/wakelock.svelte.ts @@ -29,14 +29,16 @@ export async function acquireWakeLock() { } export async function releaseWakeLock() { - if (sentinel) { - const toReleaseSentinel = sentinel; - // Unset first to avoid race condition after await - sentinel = undefined; - - // eslint-disable-next-line tscompat/tscompat - await toReleaseSentinel.release(); + if (!sentinel) { + return; } + + const toReleaseSentinel = sentinel; + // Unset first to avoid race condition after await + sentinel = undefined; + + // eslint-disable-next-line tscompat/tscompat + await toReleaseSentinel.release(); } if (isSupported) { diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 42072c122b..032a58ad52 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -178,11 +178,13 @@ }; const updateThumbnailUsingCurrentSelection = async () => { - if (assetMultiSelectManager.assets.length === 1) { - const [firstAsset] = assetMultiSelectManager.assets; - assetMultiSelectManager.clear(); - await updateThumbnail(firstAsset.id); + if (assetMultiSelectManager.assets.length !== 1) { + return; } + + const [firstAsset] = assetMultiSelectManager.assets; + assetMultiSelectManager.clear(); + await updateThumbnail(firstAsset.id); }; const updateThumbnail = async (assetId: string) => { @@ -274,10 +276,12 @@ }; const onAlbumDelete = async ({ id }: AlbumResponseDto) => { - if (id === album.id) { - await goto(Route.albums()); - viewMode = AlbumPageViewMode.VIEW; + if (id !== album.id) { + return; } + + await goto(Route.albums()); + viewMode = AlbumPageViewMode.VIEW; }; const onAlbumAddAssets = async ({ albumIds }: { albumIds: string[] }) => { diff --git a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte index 8d06d68758..dd4e776d33 100644 --- a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -31,10 +31,12 @@ const options = { visibility: AssetVisibility.Archive }; const handleEscape = () => { - if (assetMultiSelectManager.selectionActive) { - assetMultiSelectManager.clear(); + if (!assetMultiSelectManager.selectionActive) { return; } + + assetMultiSelectManager.clear(); + return; }; const handleSetVisibility = (assetIds: string[]) => { diff --git a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte index c69bd75ea9..597a1e43e1 100644 --- a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -34,10 +34,12 @@ const options = { isFavorite: true, withStacked: true }; const handleEscape = () => { - if (assetMultiSelectManager.selectionActive) { - assetMultiSelectManager.clear(); + if (!assetMultiSelectManager.selectionActive) { return; } + + assetMultiSelectManager.clear(); + return; }; const handleSetVisibility = (assetIds: string[]) => { diff --git a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte index 460f84f572..ec0afaeba0 100644 --- a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -32,10 +32,12 @@ const options = { visibility: AssetVisibility.Locked }; const handleEscape = () => { - if (assetMultiSelectManager.selectionActive) { - assetMultiSelectManager.clear(); + if (!assetMultiSelectManager.selectionActive) { return; } + + assetMultiSelectManager.clear(); + return; }; const handleMoveOffLockedFolder = (assetIds: string[]) => { diff --git a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 8e355d3efb..7b1c4d97a7 100644 --- a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -27,10 +27,12 @@ }); const handleEscape = () => { - if (assetMultiSelectManager.selectionActive) { - assetMultiSelectManager.clear(); + if (!assetMultiSelectManager.selectionActive) { return; } + + assetMultiSelectManager.clear(); + return; }; diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte index 9a672e6a27..733c1f556a 100644 --- a/web/src/routes/(user)/people/+page.svelte +++ b/web/src/routes/(user)/people/+page.svelte @@ -71,7 +71,7 @@ if (pagesToLoad) { handlePromiseError( Promise.all( - Array.from({ length: pagesToLoad }).map((_, i) => { + Array.from({ length: pagesToLoad }, (_, i) => { return getAllPeople({ withHidden: true, page: startingPage + i }); }), ).then((pages) => { diff --git a/web/src/routes/(user)/people/PeopleInfiniteScroll.svelte b/web/src/routes/(user)/people/PeopleInfiniteScroll.svelte index 54182018c4..0fc93b625a 100644 --- a/web/src/routes/(user)/people/PeopleInfiniteScroll.svelte +++ b/web/src/routes/(user)/people/PeopleInfiniteScroll.svelte @@ -20,10 +20,12 @@ }); $effect(() => { - if (lastPersonContainer) { - intersectionObserver.disconnect(); - intersectionObserver.observe(lastPersonContainer); + if (!lastPersonContainer) { + return; } + + intersectionObserver.disconnect(); + intersectionObserver.observe(lastPersonContainer); }); diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/MergeFaceSelector.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/MergeFaceSelector.svelte index 2fac6d0d11..35a30a472d 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/MergeFaceSelector.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/MergeFaceSelector.svelte @@ -37,6 +37,7 @@ onMount(handleSearch); const handleSwapPeople = async () => { + // eslint-disable-next-line unicorn/no-unreadable-array-destructuring [person, selectedPeople[0]] = [selectedPeople[0], person]; await goto(Route.viewPerson(person, { previousRoute: Route.people(), action: 'merge' })); }; diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/PeopleList.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/PeopleList.svelte index ed2a088f6e..9d3a174e9d 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/PeopleList.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/PeopleList.svelte @@ -20,8 +20,8 @@ let name = $state(''); const showPeople = $derived( - (name ? searchedPeopleLocal : people).filter( - (person) => !peopleToNotShow.some((unselectedPerson) => unselectedPerson.id === person.id), + (name ? searchedPeopleLocal : people).filter((person) => + peopleToNotShow.every((unselectedPerson) => unselectedPerson.id !== person.id), ), ); diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/UnmergeFaceSelector.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/UnmergeFaceSelector.svelte index 67a5114adb..672c4a94b7 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/UnmergeFaceSelector.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/UnmergeFaceSelector.svelte @@ -39,11 +39,10 @@ let peopleToNotShow = $derived(selectedPerson ? [personAssets, selectedPerson] : [personAssets]); - const selectedPeople: AssetFaceUpdateItem[] = []; - - for (const assetId of assetIds) { - selectedPeople.push({ assetId, personId: personAssets.id }); - } + const selectedPeople: AssetFaceUpdateItem[] = Array.from(assetIds, (assetId) => ({ + assetId, + personId: personAssets.id, + })); onMount(async () => { const data = await getAllPeople({ withHidden: false }); diff --git a/web/src/routes/(user)/people/manage/+page.svelte b/web/src/routes/(user)/people/manage/+page.svelte index 4c311f638c..692d41ebe6 100644 --- a/web/src/routes/(user)/people/manage/+page.svelte +++ b/web/src/routes/(user)/people/manage/+page.svelte @@ -30,11 +30,8 @@ const getNextVisibility = (toggleVisibility: ToggleVisibility) => { if (toggleVisibility === ToggleVisibility.SHOW_ALL) { return ToggleVisibility.HIDE_UNNANEMD; - } else if (toggleVisibility === ToggleVisibility.HIDE_UNNANEMD) { - return ToggleVisibility.HIDE_ALL; - } else { - return ToggleVisibility.SHOW_ALL; } + return toggleVisibility === ToggleVisibility.HIDE_UNNANEMD ? ToggleVisibility.HIDE_ALL : ToggleVisibility.SHOW_ALL; }; const handleToggleVisibility = () => { diff --git a/web/src/routes/(user)/places/PlacesList.svelte b/web/src/routes/(user)/places/PlacesList.svelte index e815c1c236..2f2dfed9ca 100644 --- a/web/src/routes/(user)/places/PlacesList.svelte +++ b/web/src/routes/(user)/places/PlacesList.svelte @@ -56,11 +56,8 @@ // We make sure empty albums stay at the end of the list if (a === unknownCountry) { return 1; - } else if (b === unknownCountry) { - return -1; - } else { - return a.localeCompare(b); } + return b === unknownCountry ? -1 : a.localeCompare(b); }); return sortedByCountryName.map(([country, places]) => ({ diff --git a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte index 27b9eeea61..228cd58c3a 100644 --- a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -32,10 +32,12 @@ } const handleEscape = () => { - if (assetMultiSelectManager.selectionActive) { - assetMultiSelectManager.clear(); + if (!assetMultiSelectManager.selectionActive) { return; } + + assetMultiSelectManager.clear(); + return; }; const { Empty, RestoreAll } = $derived(getTrashActions($t)); diff --git a/web/src/routes/(user)/user-settings/DownloadSettings.svelte b/web/src/routes/(user)/user-settings/DownloadSettings.svelte index f201991ef1..e4cb1f0d63 100644 --- a/web/src/routes/(user)/user-settings/DownloadSettings.svelte +++ b/web/src/routes/(user)/user-settings/DownloadSettings.svelte @@ -11,7 +11,7 @@ import { fade } from 'svelte/transition'; let archiveSize = $state(convertFromBytes(authManager.preferences.download.archiveSize || 4, ByteUnit.GiB)); - let includeEmbeddedVideos = $state(authManager.preferences.download.includeEmbeddedVideos || false); + let includeEmbeddedVideos = $state(authManager.preferences.download.includeEmbeddedVideos); const handleSave = async () => { try { diff --git a/web/src/routes/(user)/user-settings/OauthSettings.svelte b/web/src/routes/(user)/user-settings/OauthSettings.svelte index 6e63ef38c7..0da42b2b2d 100644 --- a/web/src/routes/(user)/user-settings/OauthSettings.svelte +++ b/web/src/routes/(user)/user-settings/OauthSettings.svelte @@ -12,10 +12,10 @@ let loading = $state(true); onMount(async () => { - if (oauth.isCallback(globalThis.location)) { + if (oauth.isCallback(location)) { try { loading = true; - const response = await oauth.link(globalThis.location); + const response = await oauth.link(location); authManager.setUser(response); toastManager.primary($t('linked_oauth_account')); } catch (error) { @@ -50,9 +50,7 @@ {#if authManager.user.oauthId} {:else} - + {/if} {/if}
diff --git a/web/src/routes/(user)/user-settings/UserSettingsList.svelte b/web/src/routes/(user)/user-settings/UserSettingsList.svelte index 97e2b366c6..91c32560d8 100644 --- a/web/src/routes/(user)/user-settings/UserSettingsList.svelte +++ b/web/src/routes/(user)/user-settings/UserSettingsList.svelte @@ -43,8 +43,7 @@ let { keys = $bindable([]), sessions = $bindable([]) }: Props = $props(); let oauthOpen = - oauth.isCallback(globalThis.location) || - $page.url.searchParams.get(QueryParameter.OPEN_SETTING) === OpenQueryParam.OAUTH; + oauth.isCallback(location) || $page.url.searchParams.get(QueryParameter.OPEN_SETTING) === OpenQueryParam.OAUTH; { const indexParam = page.url.searchParams.get('index') ?? '0'; - const parsedIndex = Number.parseInt(indexParam, 10); + const parsedIndex = Math.trunc(Number(indexParam)); return correctDuplicatesIndex(Number.isNaN(parsedIndex) ? 0 : parsedIndex); })(), ); diff --git a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/DuplicatesCompareControl.svelte b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/DuplicatesCompareControl.svelte index f517b6f8f5..d6a5901fcf 100644 --- a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/DuplicatesCompareControl.svelte +++ b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/DuplicatesCompareControl.svelte @@ -56,7 +56,7 @@ }); const onRandom = async () => { - if (assets.length <= 0) { + if (assets.length === 0) { return; } const index = Math.floor(Math.random() * assets.length); diff --git a/web/src/routes/(user)/utilities/geolocation/+page.svelte b/web/src/routes/(user)/utilities/geolocation/+page.svelte index d7c83bb042..4613a214e9 100644 --- a/web/src/routes/(user)/utilities/geolocation/+page.svelte +++ b/web/src/routes/(user)/utilities/geolocation/+page.svelte @@ -98,10 +98,12 @@ point = selected; }; const handleEscape = () => { - if (assetMultiSelectManager.selectionActive) { - assetMultiSelectManager.clear(); + if (!assetMultiSelectManager.selectionActive) { return; } + + assetMultiSelectManager.clear(); + return; }; type AssetPoint = { latitude: number; longitude: number }; diff --git a/web/src/routes/(user)/utilities/large-files/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/utilities/large-files/[[photos=photos]]/[[assetId=id]]/+page.svelte index 21ebd40f37..f4c0a93655 100644 --- a/web/src/routes/(user)/utilities/large-files/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/utilities/large-files/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -28,7 +28,7 @@ }); const onRandom = async () => { - if (assets.length <= 0) { + if (assets.length === 0) { return undefined; } const index = Math.floor(Math.random() * assets.length); diff --git a/web/src/routes/(user)/workflows/[workflowId]/+page.svelte b/web/src/routes/(user)/workflows/[workflowId]/+page.svelte index 63942153d5..13bc645979 100644 --- a/web/src/routes/(user)/workflows/[workflowId]/+page.svelte +++ b/web/src/routes/(user)/workflows/[workflowId]/+page.svelte @@ -189,11 +189,13 @@ }; const onWorkflowUpdate = async (response: WorkflowResponseDto) => { - if (id === response.id) { - data.workflow = response; - savedWorkflow = cloneDeep(response); - await invalidate('workflow:data'); + if (id !== response.id) { + return; } + + data.workflow = response; + savedWorkflow = cloneDeep(response); + await invalidate('workflow:data'); }; const onWorkflowDelete = async (response: WorkflowResponseDto) => { @@ -252,10 +254,12 @@ } void confirmNavigation().then((confirmed) => { - if (confirmed) { - allowNavigation = true; - void goto(to.url); + if (!confirmed) { + return; } + + allowNavigation = true; + void goto(to.url); }); }); diff --git a/web/src/routes/(user)/workflows/[workflowId]/WorkflowSummary.svelte b/web/src/routes/(user)/workflows/[workflowId]/WorkflowSummary.svelte index 3d6e29e896..16e9e1b041 100644 --- a/web/src/routes/(user)/workflows/[workflowId]/WorkflowSummary.svelte +++ b/web/src/routes/(user)/workflows/[workflowId]/WorkflowSummary.svelte @@ -53,7 +53,7 @@ const asciiSummary = $derived.by(() => { const lines: string[] = []; const title = workflow.name ?? $t('no_name'); - lines.push(`${title}`); + lines.push(title); if (workflow.description) { lines.push(workflow.description); } diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index ec20a1d866..f07b2774a5 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -250,10 +250,7 @@ {#if page.data.meta.imageUrl} {/if} {/if} diff --git a/web/src/routes/admin/maintenance/+page.svelte b/web/src/routes/admin/maintenance/+page.svelte index 44f53c8b6a..f7af961cbb 100644 --- a/web/src/routes/admin/maintenance/+page.svelte +++ b/web/src/routes/admin/maintenance/+page.svelte @@ -98,10 +98,12 @@ }); const onJobCreate = ({ dto }: { dto: JobCreateDto }) => { - if ((Object.values(jobNames).includes(dto.name) || Object.values(refreshJobNames).includes(dto.name)) && jobs) { - activeJobs.add(dto.name); - jobs.integrityCheck.queueStatus.isActive = true; + if (!((Object.values(jobNames).includes(dto.name) || Object.values(refreshJobNames).includes(dto.name)) && jobs)) { + return; } + + activeJobs.add(dto.name); + jobs.integrityCheck.queueStatus.isActive = true; }; @@ -165,12 +167,7 @@ disabled={activeJobs.has(refreshJobNames[reportType])}>{$t('refresh')} - + {/snippet} diff --git a/web/src/routes/admin/system-settings/AuthSettings.svelte b/web/src/routes/admin/system-settings/AuthSettings.svelte index 651f7b3803..22118d97a8 100644 --- a/web/src/routes/admin/system-settings/AuthSettings.svelte +++ b/web/src/routes/admin/system-settings/AuthSettings.svelte @@ -24,7 +24,7 @@ // click runs before bind const previouslyEnabled = configToEdit.oauth.mobileOverrideEnabled; if (!previouslyEnabled && !configToEdit.oauth.mobileRedirectUri) { - configToEdit.oauth.mobileRedirectUri = globalThis.location.origin + '/api/oauth/mobile-redirect'; + configToEdit.oauth.mobileRedirectUri = location.origin + '/api/oauth/mobile-redirect'; } }; @@ -102,7 +102,7 @@ bind:value={configToEdit.oauth.issuerUrl} required={true} disabled={disabled || !configToEdit.oauth.enabled} - isEdited={!(configToEdit.oauth.issuerUrl === config.oauth.issuerUrl)} + isEdited={configToEdit.oauth.issuerUrl !== config.oauth.issuerUrl} /> {#if configToEdit.oauth.clientSecret} @@ -128,7 +128,7 @@ label="token_endpoint_auth_method" bind:value={configToEdit.oauth.tokenEndpointAuthMethod} disabled={disabled || !configToEdit.oauth.enabled || !configToEdit.oauth.clientSecret} - isEdited={!(configToEdit.oauth.tokenEndpointAuthMethod === config.oauth.tokenEndpointAuthMethod)} + isEdited={configToEdit.oauth.tokenEndpointAuthMethod !== config.oauth.tokenEndpointAuthMethod} options={[ { value: OAuthTokenEndpointAuthMethod.ClientSecretPost, text: 'client_secret_post' }, { value: OAuthTokenEndpointAuthMethod.ClientSecretBasic, text: 'client_secret_basic' }, @@ -143,7 +143,7 @@ bind:value={configToEdit.oauth.scope} required={true} disabled={disabled || !configToEdit.oauth.enabled} - isEdited={!(configToEdit.oauth.scope === config.oauth.scope)} + isEdited={configToEdit.oauth.scope !== config.oauth.scope} /> {/if} {/if} diff --git a/web/src/routes/admin/system-settings/FFmpegSettings.svelte b/web/src/routes/admin/system-settings/FFmpegSettings.svelte index 2e173eaa7e..f0436e0bf7 100644 --- a/web/src/routes/admin/system-settings/FFmpegSettings.svelte +++ b/web/src/routes/admin/system-settings/FFmpegSettings.svelte @@ -178,7 +178,7 @@ onSelect={() => configToEdit.ffmpeg.acceptedAudioCodecs.includes(configToEdit.ffmpeg.targetAudioCodec) ? null - : configToEdit.ffmpeg.acceptedAudioCodecs.push(configToEdit.ffmpeg.targetAudioCodec)} + : void configToEdit.ffmpeg.acceptedAudioCodecs.push(configToEdit.ffmpeg.targetAudioCodec)} /> {#snippet descriptionSnippet()}

@@ -79,10 +77,8 @@ bind:value={configToEdit.integrityChecks.untrackedFiles.cronExpression} required={true} {disabled} - isEdited={!( - configToEdit.integrityChecks.untrackedFiles.cronExpression === - config.integrityChecks.untrackedFiles.cronExpression - )} + isEdited={configToEdit.integrityChecks.untrackedFiles.cronExpression !== + config.integrityChecks.untrackedFiles.cronExpression} > {#snippet descriptionSnippet()}

@@ -120,10 +116,8 @@ bind:value={configToEdit.integrityChecks.checksumFiles.cronExpression} required={true} {disabled} - isEdited={!( - configToEdit.integrityChecks.checksumFiles.cronExpression === - config.integrityChecks.checksumFiles.cronExpression - )} + isEdited={configToEdit.integrityChecks.checksumFiles.cronExpression !== + config.integrityChecks.checksumFiles.cronExpression} > {#snippet descriptionSnippet()}

diff --git a/web/src/routes/admin/system-settings/JobSettings.svelte b/web/src/routes/admin/system-settings/JobSettings.svelte index a2d51e0a4a..a43be5e9f4 100644 --- a/web/src/routes/admin/system-settings/JobSettings.svelte +++ b/web/src/routes/admin/system-settings/JobSettings.svelte @@ -27,7 +27,7 @@ ]; function isSystemConfigJobDto(jobName: string): jobName is keyof SystemConfigJobDto { - return jobName in configToEdit.job; + return Object.hasOwn(configToEdit.job, jobName); } const queueTitles: Record = $derived({ @@ -66,7 +66,7 @@ description="" bind:value={configToEdit.job[queueName].concurrency} required={true} - isEdited={!(configToEdit.job[queueName].concurrency == config.job[queueName].concurrency)} + isEdited={configToEdit.job[queueName].concurrency != config.job[queueName].concurrency} /> {:else} configToEdit.machineLearning.urls.push('')} + onclick={() => void configToEdit.machineLearning.urls.push('')} disabled={disabled || !configToEdit.machineLearning.enabled}>{$t('add_url')}

diff --git a/web/src/routes/admin/system-settings/NightlyTasksSettings.svelte b/web/src/routes/admin/system-settings/NightlyTasksSettings.svelte index 1d5debe818..88290a0f49 100644 --- a/web/src/routes/admin/system-settings/NightlyTasksSettings.svelte +++ b/web/src/routes/admin/system-settings/NightlyTasksSettings.svelte @@ -24,7 +24,7 @@ bind:value={configToEdit.nightlyTasks.startTime} required={true} {disabled} - isEdited={!(configToEdit.nightlyTasks.startTime === config.nightlyTasks.startTime)} + isEdited={configToEdit.nightlyTasks.startTime !== config.nightlyTasks.startTime} /> { - if (update.id === user.id) { - data.user = update; - await invalidateAll(); + if (update.id !== user.id) { + return; } + + data.user = update; + await invalidateAll(); }; const onUserAdminDeleted = async ({ id }: { id: string }) => { diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte index 1a73dfb890..78cd5fdba3 100644 --- a/web/src/routes/auth/login/+page.svelte +++ b/web/src/routes/auth/login/+page.svelte @@ -42,9 +42,9 @@ return; } - if (oauth.isCallback(globalThis.location)) { + if (oauth.isCallback(location)) { try { - const user = await oauth.login(globalThis.location); + const user = await oauth.login(location); if (!user.isOnboarded) { await onOnboarding(); @@ -63,11 +63,11 @@ try { if ( - (featureFlagsManager.value.oauthAutoLaunch && !oauth.isAutoLaunchDisabled(globalThis.location)) || - oauth.isAutoLaunchEnabled(globalThis.location) + (featureFlagsManager.value.oauthAutoLaunch && !oauth.isAutoLaunchDisabled(location)) || + oauth.isAutoLaunchEnabled(location) ) { await goto(Route.login({ autoLaunch: 0 }), { replaceState: true }); - await oauth.authorize(globalThis.location); + await oauth.authorize(location); return; } } catch (error) { @@ -113,7 +113,7 @@ const handleOAuthLogin = async () => { oauthLoading = true; oauthError = ''; - const success = await oauth.authorize(globalThis.location); + const success = await oauth.authorize(location); if (!success) { oauthLoading = false; oauthError = $t('errors.unable_to_login_with_oauth'); diff --git a/web/src/routes/maintenance/+page.svelte b/web/src/routes/maintenance/+page.svelte index c5fbde28ec..566231692d 100644 --- a/web/src/routes/maintenance/+page.svelte +++ b/web/src/routes/maintenance/+page.svelte @@ -1,4 +1,5 @@ - +
{#if loading} diff --git a/web/src/lib/modals/AssetAddToAlbumModal.svelte b/web/src/lib/modals/AssetAddToAlbumModal.svelte index b35c125d08..7259dd8245 100644 --- a/web/src/lib/modals/AssetAddToAlbumModal.svelte +++ b/web/src/lib/modals/AssetAddToAlbumModal.svelte @@ -24,4 +24,4 @@ }; - + From 4d9a27691ee00de3e519a47e1906f6e906bfbde4 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Mon, 3 Aug 2026 19:53:12 +0530 Subject: [PATCH 167/204] fix: action provider overrides (#30480) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- mobile/lib/presentation/actions/action.dart | 4 +++- .../presentation/actions/archive.action.dart | 2 +- .../presentation/actions/delete.action.dart | 4 ++-- .../presentation/actions/download.action.dart | 2 +- .../actions/edit_asset.action.dart | 2 +- .../actions/edit_datetime.action.dart | 2 +- .../actions/edit_location.action.dart | 2 +- .../presentation/actions/favorite.action.dart | 2 +- .../lib/presentation/actions/lock.action.dart | 2 +- .../actions/remove_from_album.action.dart | 2 +- .../presentation/actions/restore.action.dart | 2 +- .../actions/set_album_cover.action.dart | 2 +- .../presentation/actions/share.action.dart | 2 +- .../actions/share_link.action.dart | 2 +- .../presentation/actions/stack.action.dart | 2 +- .../lib/presentation/actions/tag.action.dart | 2 +- .../presentation/actions/upload.action.dart | 2 +- .../presentation/presentation_context.dart | 19 +++++++++++-------- 18 files changed, 31 insertions(+), 26 deletions(-) diff --git a/mobile/lib/presentation/actions/action.dart b/mobile/lib/presentation/actions/action.dart index 072c2524be..d880b7ddf7 100644 --- a/mobile/lib/presentation/actions/action.dart +++ b/mobile/lib/presentation/actions/action.dart @@ -33,6 +33,7 @@ final assetsActionProvider = Provider.family.autoDispose, null => const {}, }, }), + dependencies: [multiSelectProvider], ); final clearSelectionProvider = Provider.family.autoDispose((ref, source) { @@ -41,10 +42,11 @@ final clearSelectionProvider = Provider.family.autoDispose, ActionSource>( (ref, source) => ref.watch(assetsActionProvider(source)).owned(ref.watch(authUserProvider).id), + dependencies: [assetsActionProvider], ); abstract class AssetActionBuilder extends ActionBuilder { diff --git a/mobile/lib/presentation/actions/archive.action.dart b/mobile/lib/presentation/actions/archive.action.dart index f04611c4c0..f8e4a1e038 100644 --- a/mobile/lib/presentation/actions/archive.action.dart +++ b/mobile/lib/presentation/actions/archive.action.dart @@ -22,7 +22,7 @@ final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, .map((asset) => asset.id) .toList(growable: false); return assetIds.isEmpty ? null : (shouldArchive: shouldArchive, assetIds: assetIds); -}); +}, dependencies: [ownedAssetsActionProvider]); class ArchiveAction extends AssetActionBuilder { const ArchiveAction({required super.source}); diff --git a/mobile/lib/presentation/actions/delete.action.dart b/mobile/lib/presentation/actions/delete.action.dart index 0fe297bd03..31d01b32ef 100644 --- a/mobile/lib/presentation/actions/delete.action.dart +++ b/mobile/lib/presentation/actions/delete.action.dart @@ -40,7 +40,7 @@ final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, final trash = trashEnabled && !ownedRemote.every((asset) => asset.isTrashed || asset.isLocked); return (localIds: localIds, remoteIds: ownedRemote.map((asset) => asset.id).toList(growable: false), trash: trash); -}); +}, dependencies: [assetsActionProvider]); class DeleteAction extends AssetActionBuilder { const DeleteAction({required super.source}); @@ -149,7 +149,7 @@ final _cleanupStateProvider = Provider.family.autoDispose?, ActionS final assets = ref.watch(assetsActionProvider(source)); final assetIds = assets.backedUp().map((asset) => asset.localId).nonNulls.toList(growable: false); return assetIds.isEmpty ? null : assetIds; -}); +}, dependencies: [assetsActionProvider]); class CleanupLocalAction extends AssetActionBuilder { const CleanupLocalAction({required super.source}); diff --git a/mobile/lib/presentation/actions/download.action.dart b/mobile/lib/presentation/actions/download.action.dart index 1303fa6b03..60a1395bcd 100644 --- a/mobile/lib/presentation/actions/download.action.dart +++ b/mobile/lib/presentation/actions/download.action.dart @@ -14,7 +14,7 @@ final _stateProvider = Provider.family.autoDispose?, ActionSou final assets = ref.watch(assetsActionProvider(source)); final remote = assets.remote().toList(growable: false); return remote.isEmpty ? null : remote; -}); +}, dependencies: [assetsActionProvider]); class DownloadAction extends AssetActionBuilder { const DownloadAction({required super.source}); diff --git a/mobile/lib/presentation/actions/edit_asset.action.dart b/mobile/lib/presentation/actions/edit_asset.action.dart index d0a7e73122..9b11032df9 100644 --- a/mobile/lib/presentation/actions/edit_asset.action.dart +++ b/mobile/lib/presentation/actions/edit_asset.action.dart @@ -28,7 +28,7 @@ final _stateProvider = Provider.family.autoDispose(( final assets = ref.watch(ownedAssetsActionProvider(source)); return assets.where((asset) => asset.isEditable).singleOrNull; -}); +}, dependencies: [ownedAssetsActionProvider]); class EditAssetAction extends AssetActionBuilder { const EditAssetAction({required super.source}); diff --git a/mobile/lib/presentation/actions/edit_datetime.action.dart b/mobile/lib/presentation/actions/edit_datetime.action.dart index a3c825c4db..31de23d1a7 100644 --- a/mobile/lib/presentation/actions/edit_datetime.action.dart +++ b/mobile/lib/presentation/actions/edit_datetime.action.dart @@ -21,7 +21,7 @@ final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, } return (assetIds: assets.map((asset) => asset.id).toList(growable: false), origin: assets.singleOrNull); -}); +}, dependencies: [ownedAssetsActionProvider]); class EditDateTimeAction extends AssetActionBuilder { const EditDateTimeAction({required super.source}); diff --git a/mobile/lib/presentation/actions/edit_location.action.dart b/mobile/lib/presentation/actions/edit_location.action.dart index f83a98099c..5ce74a0f40 100644 --- a/mobile/lib/presentation/actions/edit_location.action.dart +++ b/mobile/lib/presentation/actions/edit_location.action.dart @@ -21,7 +21,7 @@ final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, } return (assetIds: assets.map((asset) => asset.id).toList(growable: false), origin: assets.singleOrNull); -}); +}, dependencies: [ownedAssetsActionProvider]); class EditLocationAction extends AssetActionBuilder { const EditLocationAction({required super.source}); diff --git a/mobile/lib/presentation/actions/favorite.action.dart b/mobile/lib/presentation/actions/favorite.action.dart index 17c841b745..402d2f3833 100644 --- a/mobile/lib/presentation/actions/favorite.action.dart +++ b/mobile/lib/presentation/actions/favorite.action.dart @@ -18,7 +18,7 @@ final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, final shouldFavorite = assets.favorite(isFavorite: false).isNotEmpty; final assetIds = assets.favorite(isFavorite: !shouldFavorite).map((asset) => asset.id).toList(growable: false); return (shouldFavorite: shouldFavorite, assetIds: assetIds); -}); +}, dependencies: [ownedAssetsActionProvider]); class FavoriteAction extends AssetActionBuilder { const FavoriteAction({required super.source}); diff --git a/mobile/lib/presentation/actions/lock.action.dart b/mobile/lib/presentation/actions/lock.action.dart index b7fd01ad18..3d090b712d 100644 --- a/mobile/lib/presentation/actions/lock.action.dart +++ b/mobile/lib/presentation/actions/lock.action.dart @@ -23,7 +23,7 @@ final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, // Only locking has an on-device copy to clean up; unlocking leaves the device alone. localIds: shouldLock ? targets.map((asset) => asset.localId).nonNulls.toList(growable: false) : const [], ); -}); +}, dependencies: [ownedAssetsActionProvider]); class LockAction extends AssetActionBuilder { const LockAction({required super.source}); diff --git a/mobile/lib/presentation/actions/remove_from_album.action.dart b/mobile/lib/presentation/actions/remove_from_album.action.dart index 3d648a9cfd..9e2d9d582f 100644 --- a/mobile/lib/presentation/actions/remove_from_album.action.dart +++ b/mobile/lib/presentation/actions/remove_from_album.action.dart @@ -11,7 +11,7 @@ final _stateProvider = Provider.family.autoDispose?, ActionSource>( final assets = ref.watch(assetsActionProvider(source)); final assetIds = assets.remote().map((asset) => asset.id).toList(growable: false); return assetIds.isEmpty ? null : assetIds; -}); +}, dependencies: [assetsActionProvider]); class RemoveFromAlbumAction extends AssetActionBuilder { final String albumId; diff --git a/mobile/lib/presentation/actions/restore.action.dart b/mobile/lib/presentation/actions/restore.action.dart index 0a2f34abf8..0a1b707b39 100644 --- a/mobile/lib/presentation/actions/restore.action.dart +++ b/mobile/lib/presentation/actions/restore.action.dart @@ -11,7 +11,7 @@ final _stateProvider = Provider.family.autoDispose?, ActionSource>( final assets = ref.watch(ownedAssetsActionProvider(source)); final assetIds = assets.trashed().map((asset) => asset.id).toList(growable: false); return assetIds.isEmpty ? null : assetIds; -}); +}, dependencies: [ownedAssetsActionProvider]); class RestoreAction extends AssetActionBuilder { const RestoreAction({required super.source}); diff --git a/mobile/lib/presentation/actions/set_album_cover.action.dart b/mobile/lib/presentation/actions/set_album_cover.action.dart index 0c16c9a9db..d5ad367eed 100644 --- a/mobile/lib/presentation/actions/set_album_cover.action.dart +++ b/mobile/lib/presentation/actions/set_album_cover.action.dart @@ -11,7 +11,7 @@ import 'package:immich_mobile/utils/error_handler.dart'; final _stateProvider = Provider.family.autoDispose((ref, source) { final assets = ref.watch(assetsActionProvider(source)); return assets.remote().map((asset) => asset.id).singleOrNull; -}); +}, dependencies: [assetsActionProvider]); class SetAlbumCoverAction extends AssetActionBuilder { final String albumId; diff --git a/mobile/lib/presentation/actions/share.action.dart b/mobile/lib/presentation/actions/share.action.dart index 4c36493265..3b8a6318de 100644 --- a/mobile/lib/presentation/actions/share.action.dart +++ b/mobile/lib/presentation/actions/share.action.dart @@ -16,7 +16,7 @@ final _stateProvider = Provider.family.autoDispose?, ActionSourc final assets = ref.watch(assetsActionProvider(source)); final shareable = assets.toList(growable: false); return shareable.isEmpty ? null : shareable; -}); +}, dependencies: [assetsActionProvider]); class ShareAction extends AssetActionBuilder { const ShareAction({required super.source}); diff --git a/mobile/lib/presentation/actions/share_link.action.dart b/mobile/lib/presentation/actions/share_link.action.dart index 1966dd8811..4285105ed3 100644 --- a/mobile/lib/presentation/actions/share_link.action.dart +++ b/mobile/lib/presentation/actions/share_link.action.dart @@ -12,7 +12,7 @@ final _stateProvider = Provider.family.autoDispose?, ActionSource>( final assets = ref.watch(assetsActionProvider(source)); final remoteIds = assets.remote().map((asset) => asset.id).toList(growable: false); return remoteIds.isEmpty ? null : remoteIds; -}); +}, dependencies: [assetsActionProvider]); class ShareLinkAction extends AssetActionBuilder { const ShareLinkAction({required super.source}); diff --git a/mobile/lib/presentation/actions/stack.action.dart b/mobile/lib/presentation/actions/stack.action.dart index 9697dc02be..5ab978a7e4 100644 --- a/mobile/lib/presentation/actions/stack.action.dart +++ b/mobile/lib/presentation/actions/stack.action.dart @@ -23,7 +23,7 @@ final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, assetIds: assets.map((asset) => asset.id).toList(growable: false), stackIds: assets.map((asset) => asset.stackId).nonNulls.toList(growable: false), ); -}); +}, dependencies: [ownedAssetsActionProvider]); class StackAction extends AssetActionBuilder { const StackAction({required super.source}); diff --git a/mobile/lib/presentation/actions/tag.action.dart b/mobile/lib/presentation/actions/tag.action.dart index 749b23d150..978648708f 100644 --- a/mobile/lib/presentation/actions/tag.action.dart +++ b/mobile/lib/presentation/actions/tag.action.dart @@ -21,7 +21,7 @@ final _stateProvider = Provider.family.autoDispose?, ActionSource>( final assets = ref.watch(ownedAssetsActionProvider(source)); final assetIds = assets.map((asset) => asset.id).toList(growable: false); return assetIds.isEmpty ? null : assetIds; -}); +}, dependencies: [ownedAssetsActionProvider]); class TagAction extends AssetActionBuilder { const TagAction({required super.source}); diff --git a/mobile/lib/presentation/actions/upload.action.dart b/mobile/lib/presentation/actions/upload.action.dart index ceb35c8786..e52659beb7 100644 --- a/mobile/lib/presentation/actions/upload.action.dart +++ b/mobile/lib/presentation/actions/upload.action.dart @@ -16,7 +16,7 @@ final _stateProvider = Provider.family.autoDispose?, ActionSour final assets = ref.watch(assetsActionProvider(source)); final local = assets.backedUp(isBackedUp: false).local().toList(growable: false); return local.isEmpty ? null : local; -}); +}, dependencies: [assetsActionProvider]); class UploadAction extends AssetActionBuilder { final bool showProgress; diff --git a/mobile/test/unit/presentation/presentation_context.dart b/mobile/test/unit/presentation/presentation_context.dart index 5b23890ef8..36ae8d087d 100644 --- a/mobile/test/unit/presentation/presentation_context.dart +++ b/mobile/test/unit/presentation/presentation_context.dart @@ -101,15 +101,18 @@ extension PumpPresentationWidget on WidgetTester { useFallbackTranslations: true, assetLoader: const CodegenLoader(), child: ProviderScope( - overrides: [...context.overrides, ...overrides], + overrides: context.overrides, child: Builder( - builder: (context) => MaterialApp( - debugShowCheckedModeBanner: false, - scaffoldMessengerKey: scaffoldMessengerKey, - localizationsDelegates: context.localizationDelegates, - supportedLocales: context.supportedLocales, - locale: context.locale, - home: Scaffold(body: widget), + builder: (context) => ProviderScope( + overrides: overrides, + child: MaterialApp( + debugShowCheckedModeBanner: false, + scaffoldMessengerKey: scaffoldMessengerKey, + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, + locale: context.locale, + home: Scaffold(body: widget), + ), ), ), ), From e5c3bdad17da1c70bc59d2d09398f8b35d820746 Mon Sep 17 00:00:00 2001 From: Santo Shakil Date: Mon, 3 Aug 2026 20:23:29 +0600 Subject: [PATCH 168/204] fix(mobile): sync stack changes from the websocket (#30479) --- mobile/lib/providers/websocket.provider.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mobile/lib/providers/websocket.provider.dart b/mobile/lib/providers/websocket.provider.dart index 2eb8ddc2b4..c6ed09360a 100644 --- a/mobile/lib/providers/websocket.provider.dart +++ b/mobile/lib/providers/websocket.provider.dart @@ -103,7 +103,8 @@ class WebsocketNotifier extends StateNotifier { socket.on('AssetUploadReadyV2', _handleSyncAssetUploadReadyV2); socket.on('AssetEditReadyV1', _handleSyncAssetEditReadyV1); socket.on('AssetEditReadyV2', _handleSyncAssetEditReadyV2); - socket.on('on_album_update', _handleAlbumUpdate); + socket.on('on_album_update', _handleRemoteChange); + socket.on('on_asset_stack_update', _handleRemoteChange); socket.on('on_config_update', _handleOnConfigUpdate); socket.on('on_new_release', _handleReleaseUpdates); } catch (e) { @@ -185,7 +186,7 @@ class WebsocketNotifier extends StateNotifier { unawaited(_ref.read(backgroundSyncProvider).syncWebsocketEditV1(data)); } - void _handleAlbumUpdate(dynamic _) { + void _handleRemoteChange(dynamic _) { unawaited(_ref.read(backgroundSyncProvider).syncRemote()); } From 46c42e0935bb5eab65e395478623eb65f097df2b Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Mon, 3 Aug 2026 16:50:28 +0200 Subject: [PATCH 169/204] chore: delete mergify config (#30521) --- .mergify.yml | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .mergify.yml diff --git a/.mergify.yml b/.mergify.yml deleted file mode 100644 index 12f3ef6715..0000000000 --- a/.mergify.yml +++ /dev/null @@ -1,7 +0,0 @@ -merge_queue: - status_comments: outcomes - -queue_rules: - - name: default - batch_size: 3 - batch_max_wait_time: 2 min From 0d7147dceca9290c5f8b4fe8b3e3b138aac2afc2 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Mon, 3 Aug 2026 18:18:24 +0200 Subject: [PATCH 170/204] fix: metadata extraction as LensModel can be a float (#30512) --- .../src/repositories/metadata.repository.ts | 6 +++++- server/src/services/metadata.service.ts | 4 +++- .../specs/services/metadata.service.spec.ts | 19 +++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts index 94047bf326..1d504f6c71 100644 --- a/server/src/repositories/metadata.repository.ts +++ b/server/src/repositories/metadata.repository.ts @@ -20,7 +20,8 @@ type TagsWithWrongTypes = | 'TagsList' | 'Keywords' | 'HierarchicalSubject' - | 'ISO'; + | 'ISO' + | 'LensModel'; export interface ImmichTags extends Omit { ContentIdentifier?: string; @@ -43,6 +44,9 @@ export interface ImmichTags extends Omit { Description?: StringOrNumber; ImageDescription?: StringOrNumber; + // Apparently LensModel can also be a float: https://github.com/immich-app/immich/issues/30492 + LensModel?: StringOrNumber; + // Extended properties for image regions, such as faces RegionInfo?: { AppliedToDimensions: { diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 171dcfe514..37dd92e27d 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -117,7 +117,9 @@ const validateRange = (value: number | undefined, min: number, max: number): Non }; const getLensModel = (exifTags: ImmichTags): string | null => { - const lensModel = (exifTags.LensID ?? exifTags.LensType ?? exifTags.LensSpec ?? exifTags.LensModel ?? '').trim(); + const lensModel = String( + exifTags.LensID ?? exifTags.LensType ?? exifTags.LensSpec ?? exifTags.LensModel ?? '', + ).trim(); if (lensModel === '----') { return null; } diff --git a/server/test/medium/specs/services/metadata.service.spec.ts b/server/test/medium/specs/services/metadata.service.spec.ts index 6dc66e3ed5..37603520f7 100644 --- a/server/test/medium/specs/services/metadata.service.spec.ts +++ b/server/test/medium/specs/services/metadata.service.spec.ts @@ -152,4 +152,23 @@ describe(MetadataService.name, () => { ).resolves.toEqual({ dateTimeOriginal: new Date('4260-03-05T04:04:12.000Z') }); }); }); + + it('should handle float lens models (#30492)', async () => { + const { sut, ctx } = setup(); + ctx.getMock(EventRepository).emit.mockResolvedValue(); + const { filePath } = await createTestFile({ LensModel: 1.8 }); + const { user } = await ctx.newUser(); + const { asset } = await ctx.newAsset({ originalPath: filePath, ownerId: user.id }); + await ctx.newExif({ assetId: asset.id, description: '' }); + + await sut.handleMetadataExtraction({ id: asset.id }); + + await expect( + ctx.database + .selectFrom('asset_exif') + .where('assetId', '=', asset.id) + .select('lensModel') + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ lensModel: '1.8' }); + }); }); From 29e7ea5302bc3f6ed1eb845706cc77cb34f40048 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Tue, 4 Aug 2026 01:40:08 +0530 Subject: [PATCH 171/204] chore(web): use FUTO F-Droid repo in utilities (#30527) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- web/src/lib/modals/AppDownloadModal.svelte | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/src/lib/modals/AppDownloadModal.svelte b/web/src/lib/modals/AppDownloadModal.svelte index 84d7630909..01a998bf5c 100644 --- a/web/src/lib/modals/AppDownloadModal.svelte +++ b/web/src/lib/modals/AppDownloadModal.svelte @@ -1,5 +1,5 @@