From d864a908117387045763c6f1ad4ba3b7a50f1bf3 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 30 Jul 2026 01:56:46 -0700 Subject: [PATCH 01/81] chore(mobile): Apply stricter linting rules for formatting (#30370) * chore(mobile): Apply stricter linting rules for formatting * Formatting fixes --- mobile/analysis_options.yaml | 31 +++++---- mobile/lib/constants/aspect_ratios.dart | 4 +- .../domain/models/asset/base_asset.model.dart | 2 +- .../lib/domain/models/asset_edit.model.dart | 2 +- .../lib/domain/models/config/app_config.dart | 4 +- .../domain/services/remote_album.service.dart | 2 - .../domain/services/sync_stream.service.dart | 2 +- .../lib/domain/services/timeline.service.dart | 2 +- mobile/lib/extensions/asset_extensions.dart | 8 +-- .../lib/extensions/collection_extensions.dart | 3 +- .../lib/extensions/datetime_extensions.dart | 14 ++-- .../infrastructure/entities/exif.entity.dart | 4 +- .../infrastructure/entities/log.entity.dart | 2 +- .../infrastructure/loaders/image_request.dart | 2 +- .../loaders/remote_image_request.dart | 9 ++- .../repositories/backup.repository.dart | 3 +- .../repositories/local_album.repository.dart | 2 +- .../repositories/ocr.repository.dart | 2 +- .../repositories/search_api.repository.dart | 46 ++++++------- .../repositories/sync_api.repository.dart | 4 +- .../repositories/sync_stream.repository.dart | 8 +-- .../trashed_local_asset.repository.dart | 2 +- .../infrastructure/utils/exif.converter.dart | 4 +- mobile/lib/main.dart | 13 ++-- .../lib/pages/backup/drift_backup.page.dart | 2 +- .../drift_backup_album_selection.page.dart | 10 +-- .../drift_backup_asset_detail.page.dart | 2 +- .../backup/drift_upload_detail.page.dart | 2 +- mobile/lib/pages/common/app_log.page.dart | 4 +- .../lib/pages/common/app_log_detail.page.dart | 6 +- mobile/lib/pages/common/download_panel.dart | 2 +- .../pages/common/headers_settings.page.dart | 4 +- .../lib/pages/common/splash_screen.page.dart | 4 +- .../lib/pages/library/folder/folder.page.dart | 4 +- .../pages/library/locked/pin_auth.page.dart | 2 +- mobile/lib/pages/login/login.page.dart | 6 +- .../search/map/map_location_picker.page.dart | 4 +- .../pages/share_intent/share_intent.page.dart | 4 +- .../pages/dev/main_timeline.page.dart | 2 +- .../pages/download_info.page.dart | 4 +- .../pages/drift_activities.page.dart | 2 +- .../pages/drift_album_options.page.dart | 16 ++--- .../pages/drift_library.page.dart | 4 +- .../presentation/pages/drift_memory.page.dart | 16 ++--- .../pages/drift_partner_detail.page.dart | 2 +- .../pages/drift_people_collection.page.dart | 2 +- .../presentation/pages/drift_person.page.dart | 2 +- .../pages/drift_remote_album.page.dart | 6 +- .../pages/drift_slideshow.page.dart | 12 ++-- .../presentation/pages/drift_trash.page.dart | 4 +- .../pages/drift_user_selection.page.dart | 12 ++-- .../pages/edit/drift_edit.page.dart | 2 +- .../pages/edit/editor.provider.dart | 2 +- .../pages/search/drift_search.page.dart | 68 +++++++++---------- .../add_action_button.widget.dart | 22 +++--- .../delete_action_button.widget.dart | 2 +- .../delete_local_action_button.widget.dart | 4 +- ...delete_permanent_action_button.widget.dart | 2 +- .../delete_trash_action_button.widget.dart | 2 +- .../download_action_button.widget.dart | 4 +- .../edit_date_time_action_button.widget.dart | 2 +- .../edit_location_action_button.widget.dart | 2 +- .../favorite_action_button.widget.dart | 2 +- .../like_activity_action_button.widget.dart | 2 +- .../open_in_browser_action_button.widget.dart | 5 +- ...emove_from_album_action_button.widget.dart | 2 +- ...from_lock_folder_action_button.widget.dart | 2 +- .../restore_action_button.widget.dart | 2 +- .../restore_trash_action_button.widget.dart | 2 +- .../set_album_cover.widget.dart | 2 +- .../share_action_button.widget.dart | 6 +- .../share_link_action_button.widget.dart | 2 +- .../similar_photos_action_button.widget.dart | 2 +- .../stack_action_button.widget.dart | 2 +- .../trash_action_button.widget.dart | 2 +- .../unarchive_action_button.widget.dart | 4 +- .../unfavorite_action_button.widget.dart | 2 +- .../unstack_action_button.widget.dart | 2 +- .../upload_action_button.widget.dart | 2 +- .../widgets/album/album_selector.widget.dart | 6 +- .../appears_in_details.widget.dart | 3 +- .../location_details.widget.dart | 2 +- .../asset_details/rating_details.widget.dart | 2 +- .../technical_details.widget.dart | 14 ++-- .../asset_viewer/asset_stack.widget.dart | 7 +- .../asset_viewer/asset_viewer.page.dart | 4 +- .../asset_viewer/rating_bar.widget.dart | 10 +-- .../asset_viewer/video_viewer.widget.dart | 6 +- .../viewer_bottom_app_bar.widget.dart | 5 +- .../viewer_top_app_bar.widget.dart | 3 +- .../base_bottom_sheet.widget.dart | 2 +- .../bottom_sheet/map_bottom_sheet.widget.dart | 6 +- .../feature_message_dialog.widget.dart | 4 +- .../widgets/images/full_image.widget.dart | 2 +- .../widgets/images/image_provider.dart | 2 +- .../widgets/images/thumbnail_tile.widget.dart | 2 +- .../presentation/widgets/map/map_utils.dart | 4 +- .../widgets/memory/memory_card.widget.dart | 2 +- .../person_edit_birthday_modal.widget.dart | 4 +- .../people/person_edit_name_modal.widget.dart | 4 +- .../people/person_option_sheet.widget.dart | 2 +- .../widgets/timeline/header.widget.dart | 2 +- .../widgets/timeline/scrubber.widget.dart | 2 +- .../widgets/timeline/timeline.widget.dart | 2 +- .../providers/album/album_title.provider.dart | 4 +- .../providers/app_life_cycle.provider.dart | 2 +- .../asset_viewer/download.provider.dart | 4 +- .../share_intent_upload.provider.dart | 4 +- mobile/lib/providers/auth.provider.dart | 2 +- .../backup/drift_backup.provider.dart | 7 +- mobile/lib/providers/cast.provider.dart | 1 - .../gallery_permission.provider.dart | 2 +- .../providers/haptic_feedback.provider.dart | 10 +-- .../infrastructure/action.provider.dart | 4 +- .../infrastructure/timeline.provider.dart | 2 +- mobile/lib/providers/local_auth.provider.dart | 3 - .../providers/map/map_marker.provider.dart | 4 +- mobile/lib/providers/oauth.provider.dart | 2 +- .../lib/providers/server_info.provider.dart | 12 ++-- .../lib/providers/sync_status.provider.dart | 2 +- .../timeline/multiselect.provider.dart | 6 +- .../upload_profile_image.provider.dart | 4 +- mobile/lib/providers/user.provider.dart | 2 +- mobile/lib/providers/websocket.provider.dart | 6 +- .../repositories/asset_media.repository.dart | 6 +- .../lib/repositories/auth_api.repository.dart | 2 +- .../drift_album_api_repository.dart | 6 +- mobile/lib/repositories/gcast.repository.dart | 2 +- .../lib/repositories/upload.repository.dart | 6 +- mobile/lib/routing/duplicate_guard.dart | 2 +- mobile/lib/routing/locked_guard.dart | 5 +- mobile/lib/routing/router.dart | 2 +- mobile/lib/services/api.service.dart | 6 +- mobile/lib/services/auth.service.dart | 2 +- .../services/background_upload.service.dart | 8 +-- mobile/lib/services/download.service.dart | 2 +- mobile/lib/services/folder.service.dart | 12 ++-- .../services/foreground_upload.service.dart | 4 +- mobile/lib/services/gcast.service.dart | 8 +-- mobile/lib/services/search.service.dart | 2 +- mobile/lib/services/server_info.service.dart | 8 +-- mobile/lib/theme/dynamic_theme.dart | 3 +- mobile/lib/utils/bytes_units.dart | 2 +- mobile/lib/utils/diff.dart | 6 +- mobile/lib/utils/editor.utils.dart | 8 +-- mobile/lib/utils/error_handler.dart | 2 +- .../lib/utils/hooks/crop_controller_hook.dart | 5 +- mobile/lib/utils/image_converter.dart | 2 +- mobile/lib/utils/image_url_builder.dart | 2 +- mobile/lib/utils/map_utils.dart | 4 +- mobile/lib/utils/openapi_patching.dart | 4 +- mobile/lib/utils/people.utils.dart | 4 +- mobile/lib/utils/timezone.dart | 2 +- .../album/remote_album_shared_user_icons.dart | 4 +- .../asset_grid/thumbnail_placeholder.dart | 2 +- .../asset_viewer/detail_panel/exif_map.dart | 8 +-- .../widgets/asset_viewer/video_controls.dart | 2 +- .../backup/drift_album_info_list_tile.dart | 4 +- .../common/app_bar_dialog/app_bar_dialog.dart | 20 +++--- .../app_bar_dialog/app_bar_profile_info.dart | 8 +-- .../app_bar_dialog/app_bar_server_info.dart | 6 +- .../server_update_notification.dart | 8 ++- .../lib/widgets/common/date_time_picker.dart | 4 +- .../widgets/common/immich_sliver_app_bar.dart | 2 +- mobile/lib/widgets/common/immich_toast.dart | 2 +- .../widgets/common/person_sliver_app_bar.dart | 8 +-- .../common/remote_album_sliver_app_bar.dart | 4 +- .../common/selection_sliver_app_bar.dart | 2 +- mobile/lib/widgets/common/tag_picker.dart | 4 +- .../widgets/forms/change_password_form.dart | 2 +- .../lib/widgets/forms/login/login_form.dart | 20 +++--- mobile/lib/widgets/forms/pin_input.dart | 6 +- .../widgets/forms/pin_registration_form.dart | 2 +- .../widgets/forms/pin_verification_form.dart | 2 +- mobile/lib/widgets/map/asset_marker_icon.dart | 6 +- .../lib/widgets/map/map_theme_override.dart | 2 +- mobile/lib/widgets/map/map_thumbnail.dart | 2 +- .../photo_view/src/core/photo_view_core.dart | 10 +-- .../photo_view/src/photo_view_wrappers.dart | 6 +- .../filter_bottom_sheet_scaffold.dart | 2 +- .../search/search_filter/people_picker.dart | 4 +- .../search_filter/star_rating_picker.dart | 2 +- .../widgets/search/thumbnail_with_info.dart | 2 +- .../widgets/settings/advanced_settings.dart | 8 +-- .../asset_viewer_settings.dart | 2 +- .../settings/free_up_space_settings.dart | 2 +- .../external_network_preference.dart | 8 +-- .../local_network_preference.dart | 10 +-- .../settings/notification_setting.dart | 4 +- .../preference_settings/haptic_setting.dart | 4 +- .../primary_color_setting.dart | 8 +-- .../settings/settings_switch_list_tile.dart | 2 +- .../ui/lib/src/components/password_input.dart | 2 +- .../services/sync_stream_service_test.dart | 2 +- .../sync_api_repository_test.dart | 2 +- .../test/infrastructure/repository.mock.dart | 2 +- .../local_asset_repository_test.dart | 2 +- .../timeline_repository_test.dart | 2 +- .../test/modules/utils/async_mutex_test.dart | 4 +- .../modules/utils/datetime_helpers_test.dart | 12 ++-- mobile/test/modules/utils/debouncer_test.dart | 10 +-- .../modules/utils/openapi_patching_test.dart | 6 +- mobile/test/services/auth.service_test.dart | 2 +- .../background_upload.service_test.dart | 2 +- .../foreground_upload.service_test.dart | 2 +- .../unit/presentation/partner_page_test.dart | 4 +- mobile/test/unit/utils/editor_test.dart | 6 +- .../action_button_utils_test.dart | 2 +- 208 files changed, 515 insertions(+), 533 deletions(-) diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index f5ead8de2e..1a7b463913 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -26,8 +26,8 @@ linter: # producing the lint. rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + # Formatting + avoid_print: true unawaited_futures: true use_build_context_synchronously: false require_trailing_commas: true @@ -35,6 +35,21 @@ linter: prefer_const_constructors: true always_use_package_imports: true always_put_control_body_on_new_line: true + unnecessary_null_checks: true + unnecessary_parenthesis: true + prefer_final_locals: true + prefer_const_declarations: true + prefer_const_literals_to_create_immutables: true + use_super_parameters: true + directives_ordering: true + no_leading_underscores_for_local_identifiers: true + always_declare_return_types: true + avoid_void_async: true + noop_primitive_operations: true + use_named_constants: true + combinators_ordering: true + avoid_multiple_declarations_per_line: true + unnecessary_breaks: true # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options @@ -46,20 +61,12 @@ analyzer: - lib/**/*.g.dart - lib/**/*.drift.dart - # TODO: Re-enable after upgrading custom_lint - # plugins: - # - custom_lint + # NOTE: We explicitly do not use riverpod_lint as there are analyzer version conflicts between + # our Flutter version and the required old riverpod_lint 2.x errors: unawaited_futures: warning always_put_control_body_on_new_line: warning -custom_lint: - rules: - - avoid_build_context_in_providers: false - - avoid_public_notifier_properties: false - - avoid_manual_providers_as_generated_provider_dependency: false - - unsupported_provider_value: false - dart_code_metrics: rules: - banned-usage: diff --git a/mobile/lib/constants/aspect_ratios.dart b/mobile/lib/constants/aspect_ratios.dart index 7a1f46d198..9ad7b8c739 100644 --- a/mobile/lib/constants/aspect_ratios.dart +++ b/mobile/lib/constants/aspect_ratios.dart @@ -42,8 +42,8 @@ class CropAspectRatio { } } -const aspectRatioFree = CropAspectRatio(customLabel: "Free", icon: Icons.crop_free); -const aspectRatioOriginal = CropAspectRatio(customLabel: "Original", icon: Icons.crop_original); +const aspectRatioFree = CropAspectRatio.free; +const aspectRatioOriginal = CropAspectRatio.original; final aspectRatioPresets = [ CropAspectRatio.free, diff --git a/mobile/lib/domain/models/asset/base_asset.model.dart b/mobile/lib/domain/models/asset/base_asset.model.dart index ea6f0ab287..d7d74daa25 100644 --- a/mobile/lib/domain/models/asset/base_asset.model.dart +++ b/mobile/lib/domain/models/asset/base_asset.model.dart @@ -53,7 +53,7 @@ sealed class BaseAsset { if (durationMs != null) { return Duration(milliseconds: durationMs); } - return const Duration(); + return Duration.zero; } bool get hasRemote => storage == AssetState.remote || storage == AssetState.merged; diff --git a/mobile/lib/domain/models/asset_edit.model.dart b/mobile/lib/domain/models/asset_edit.model.dart index 9809b9c606..c7a09dbae3 100644 --- a/mobile/lib/domain/models/asset_edit.model.dart +++ b/mobile/lib/domain/models/asset_edit.model.dart @@ -1,4 +1,4 @@ -import "package:openapi/api.dart" show CropParameters, RotateParameters, MirrorParameters; +import "package:openapi/api.dart" show CropParameters, MirrorParameters, RotateParameters; enum AssetEditAction { rotate, crop, mirror, other } diff --git a/mobile/lib/domain/models/config/app_config.dart b/mobile/lib/domain/models/config/app_config.dart index e4e11baf9d..df147f3e3a 100644 --- a/mobile/lib/domain/models/config/app_config.dart +++ b/mobile/lib/domain/models/config/app_config.dart @@ -190,9 +190,9 @@ class AppConfig { .viewerTapToNavigate => copyWith(viewer: viewer.copyWith(tapToNavigate: value as bool)), .networkAutoEndpointSwitching => copyWith(network: network.copyWith(autoEndpointSwitching: value as bool)), .networkPreferredWifiName => copyWith( - network: network.copyWith(preferredWifiName: .fromNullable((value as String?))), + network: network.copyWith(preferredWifiName: .fromNullable(value as String?)), ), - .networkLocalEndpoint => copyWith(network: network.copyWith(localEndpoint: .fromNullable((value as String?)))), + .networkLocalEndpoint => copyWith(network: network.copyWith(localEndpoint: .fromNullable(value as String?))), .networkExternalEndpointList => copyWith(network: network.copyWith(externalEndpointList: value as List)), .networkCustomHeaders => copyWith(network: network.copyWith(customHeaders: value as Map)), .albumSortMode => copyWith(album: album.copyWith(sortMode: value as AlbumSortMode)), diff --git a/mobile/lib/domain/services/remote_album.service.dart b/mobile/lib/domain/services/remote_album.service.dart index c0bbaa8127..e59d75b01a 100644 --- a/mobile/lib/domain/services/remote_album.service.dart +++ b/mobile/lib/domain/services/remote_album.service.dart @@ -105,10 +105,8 @@ class RemoteAlbumService { switch (filterMode) { case QuickFilterMode.myAlbums: filtered = filtered.where((album) => album.ownerId == userId).toList(); - break; case QuickFilterMode.sharedWithMe: filtered = filtered.where((album) => album.ownerId != userId).toList(); - break; case QuickFilterMode.all: break; } diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index 9ebce300ba..9b4d4bb275 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -155,7 +155,7 @@ class SyncStreamService { } Future _handleEvents(List events, Function() abort, Function() reset) async { - List items = []; + final List items = []; for (final event in events) { if (isCancelled) { _logger.warning("Sync stream cancelled"); diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index 4cc58b0fe7..9b539ec218 100644 --- a/mobile/lib/domain/services/timeline.service.dart +++ b/mobile/lib/domain/services/timeline.service.dart @@ -178,7 +178,7 @@ class TimelineService { if (!hasRange(index, count)) { throw RangeError('TimelineService::getAssets Index out of range'); } - int start = index - _bufferOffset; + final int start = index - _bufferOffset; return _buffer.slice(start, start + count); } diff --git a/mobile/lib/extensions/asset_extensions.dart b/mobile/lib/extensions/asset_extensions.dart index 52d31cb0b3..ec1aa80ceb 100644 --- a/mobile/lib/extensions/asset_extensions.dart +++ b/mobile/lib/extensions/asset_extensions.dart @@ -15,8 +15,8 @@ extension DTOToAsset on api.AssetResponseDto { ownerId: ownerId, visibility: visibility.toAssetVisibility(), durationMs: duration, - height: height?.toInt(), - width: width?.toInt(), + height: height, + width: width, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId.orElse(null), thumbHash: thumbhash, @@ -38,8 +38,8 @@ extension DTOToAsset on api.AssetResponseDto { ownerId: ownerId, visibility: visibility.toAssetVisibility(), durationMs: duration, - height: height?.toInt(), - width: width?.toInt(), + height: height, + width: width, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId.orElse(null), thumbHash: thumbhash, diff --git a/mobile/lib/extensions/collection_extensions.dart b/mobile/lib/extensions/collection_extensions.dart index b861eb0570..53e8e73fd1 100644 --- a/mobile/lib/extensions/collection_extensions.dart +++ b/mobile/lib/extensions/collection_extensions.dart @@ -5,7 +5,8 @@ import 'package:collection/collection.dart'; extension ListExtension on List { List uniqueConsecutive({int Function(E a, E b)? compare, void Function(E a, E b)? onDuplicate}) { compare ??= (E a, E b) => a == b ? 0 : 1; - int i = 1, j = 1; + int i = 1; + int j = 1; for (; i < length; i++) { if (compare(this[i - 1], this[i]) != 0) { if (i != j) { diff --git a/mobile/lib/extensions/datetime_extensions.dart b/mobile/lib/extensions/datetime_extensions.dart index 0bc95565a6..6a51d15df0 100644 --- a/mobile/lib/extensions/datetime_extensions.dart +++ b/mobile/lib/extensions/datetime_extensions.dart @@ -4,7 +4,7 @@ import 'package:easy_localization/easy_localization.dart'; extension TimeAgoExtension on DateTime { /// Displays the time difference of this [DateTime] object to the current time as a [String] String timeAgo({bool numericDates = true}) { - DateTime date = toLocal(); + final DateTime date = toLocal(); final now = DateTime.now().toLocal(); final difference = now.difference(date); @@ -13,27 +13,27 @@ extension TimeAgoExtension on DateTime { } else if (difference.inSeconds < 60) { return '${difference.inSeconds} seconds ago'; } else if (difference.inMinutes <= 1) { - return (numericDates) ? '1 minute ago' : 'A minute ago'; + return numericDates ? '1 minute ago' : 'A minute ago'; } else if (difference.inMinutes < 60) { return '${difference.inMinutes} minutes ago'; } else if (difference.inHours <= 1) { - return (numericDates) ? '1 hour ago' : 'An hour ago'; + return numericDates ? '1 hour ago' : 'An hour ago'; } else if (difference.inHours < 60) { return '${difference.inHours} hours ago'; } else if (difference.inDays <= 1) { - return (numericDates) ? '1 day ago' : 'Yesterday'; + return numericDates ? '1 day ago' : 'Yesterday'; } else if (difference.inDays < 6) { return '${difference.inDays} days ago'; } else if ((difference.inDays / 7).ceil() <= 1) { - return (numericDates) ? '1 week ago' : 'Last week'; + return numericDates ? '1 week ago' : 'Last week'; } else if ((difference.inDays / 7).ceil() < 4) { return '${(difference.inDays / 7).ceil()} weeks ago'; } else if ((difference.inDays / 30).ceil() <= 1) { - return (numericDates) ? '1 month ago' : 'Last month'; + return numericDates ? '1 month ago' : 'Last month'; } else if ((difference.inDays / 30).ceil() < 30) { return '${(difference.inDays / 30).ceil()} months ago'; } else if ((difference.inDays / 365).ceil() <= 1) { - return (numericDates) ? '1 year ago' : 'Last year'; + return numericDates ? '1 year ago' : 'Last year'; } return '${(difference.inDays / 365).floor()} years ago'; } diff --git a/mobile/lib/infrastructure/entities/exif.entity.dart b/mobile/lib/infrastructure/entities/exif.entity.dart index 120fbd0c68..3328b38f31 100644 --- a/mobile/lib/infrastructure/entities/exif.entity.dart +++ b/mobile/lib/infrastructure/entities/exif.entity.dart @@ -79,8 +79,8 @@ extension RemoteExifEntityDataDomainEx on RemoteExifEntityData { orientation: orientation, latitude: latitude, longitude: longitude, - f: fNumber?.toDouble(), - mm: focalLength?.toDouble(), + f: fNumber, + mm: focalLength, lens: lens, isFlipped: ExifDtoConverter.isOrientationFlipped(orientation), exposureSeconds: ExifDtoConverter.exposureTimeToSeconds(exposureTime), diff --git a/mobile/lib/infrastructure/entities/log.entity.dart b/mobile/lib/infrastructure/entities/log.entity.dart index e578459827..264bd3331f 100644 --- a/mobile/lib/infrastructure/entities/log.entity.dart +++ b/mobile/lib/infrastructure/entities/log.entity.dart @@ -1,6 +1,6 @@ import 'package:drift/drift.dart'; -import 'package:immich_mobile/infrastructure/entities/log.entity.drift.dart'; import 'package:immich_mobile/domain/models/log.model.dart' as domain; +import 'package:immich_mobile/infrastructure/entities/log.entity.drift.dart'; class LogMessageEntity extends Table { const LogMessageEntity(); diff --git a/mobile/lib/infrastructure/loaders/image_request.dart b/mobile/lib/infrastructure/loaders/image_request.dart index d0f3679084..8b7cdc7621 100644 --- a/mobile/lib/infrastructure/loaders/image_request.dart +++ b/mobile/lib/infrastructure/loaders/image_request.dart @@ -18,7 +18,7 @@ abstract class ImageRequest { final int requestId = _nextRequestId++; bool _isCancelled = false; - get isCancelled => _isCancelled; + bool get isCancelled => _isCancelled; ImageRequest(); diff --git a/mobile/lib/infrastructure/loaders/remote_image_request.dart b/mobile/lib/infrastructure/loaders/remote_image_request.dart index 40705c16d2..d6a25753fb 100644 --- a/mobile/lib/infrastructure/loaders/remote_image_request.dart +++ b/mobile/lib/infrastructure/loaders/remote_image_request.dart @@ -14,8 +14,13 @@ class RemoteImageRequest extends ImageRequest { final info = await remoteImageApi.requestImage(uri, requestId: requestId, preferEncoded: false); // Android falls back to encoded data if native decoding fails, so check for both shapes of the response. final frame = switch (info) { - {'pointer': int pointer, 'length': int length} => await _fromEncodedPlatformImage(pointer, length), - {'pointer': int pointer, 'width': int width, 'height': int height, 'rowBytes': int rowBytes} => + {'pointer': final int pointer, 'length': final int length} => await _fromEncodedPlatformImage(pointer, length), + { + 'pointer': final int pointer, + 'width': final int width, + 'height': final int height, + 'rowBytes': final int rowBytes, + } => await _fromDecodedPlatformImage(pointer, width, height, rowBytes), _ => null, }; diff --git a/mobile/lib/infrastructure/repositories/backup.repository.dart b/mobile/lib/infrastructure/repositories/backup.repository.dart index 0241711d4b..eb3cba95d0 100644 --- a/mobile/lib/infrastructure/repositories/backup.repository.dart +++ b/mobile/lib/infrastructure/repositories/backup.repository.dart @@ -4,6 +4,7 @@ import 'package:drift/drift.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; @@ -16,7 +17,7 @@ class DriftBackupRepository extends DriftDatabaseRepository { final Drift _db; const DriftBackupRepository(this._db) : super(_db); - _getExcludedSubquery() { + JoinedSelectStatement<$LocalAlbumAssetEntityTable, LocalAlbumAssetEntityData> _getExcludedSubquery() { return _db.localAlbumAssetEntity.selectOnly() ..addColumns([_db.localAlbumAssetEntity.assetId]) ..join([ diff --git a/mobile/lib/infrastructure/repositories/local_album.repository.dart b/mobile/lib/infrastructure/repositories/local_album.repository.dart index a443ffb975..a9911fe044 100644 --- a/mobile/lib/infrastructure/repositories/local_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_album.repository.dart @@ -356,7 +356,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository { return _deleteAssets(assetIds); } - List assetsToDelete = []; + final List assetsToDelete = []; List assetsToUnLink = []; final uniqueAssets = await _getUniqueAssetsInAlbum(albumId); diff --git a/mobile/lib/infrastructure/repositories/ocr.repository.dart b/mobile/lib/infrastructure/repositories/ocr.repository.dart index 99f4fa4675..c3465df357 100644 --- a/mobile/lib/infrastructure/repositories/ocr.repository.dart +++ b/mobile/lib/infrastructure/repositories/ocr.repository.dart @@ -1,7 +1,7 @@ +import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/ocr.model.dart'; import 'package:immich_mobile/infrastructure/entities/asset_ocr.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:drift/drift.dart'; class OcrRepository extends DriftDatabaseRepository { final Drift _db; diff --git a/mobile/lib/infrastructure/repositories/search_api.repository.dart b/mobile/lib/infrastructure/repositories/search_api.repository.dart index 395d4045cf..dec6e1d167 100644 --- a/mobile/lib/infrastructure/repositories/search_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/search_api.repository.dart @@ -21,28 +21,28 @@ class SearchApiRepository extends ApiRepository { (filter.assetId != null && filter.assetId!.isNotEmpty)) { return _api.searchSmart( SmartSearchDto( - query: filter.context == null ? const Optional.absent() : Optional.present(filter.context!), - queryAssetId: filter.assetId == null ? const Optional.absent() : Optional.present(filter.assetId!), - language: filter.language == null ? const Optional.absent() : Optional.present(filter.language!), + query: filter.context == null ? const Optional.absent() : Optional.present(filter.context), + queryAssetId: filter.assetId == null ? const Optional.absent() : Optional.present(filter.assetId), + language: filter.language == null ? const Optional.absent() : Optional.present(filter.language), country: filter.location.country == null ? const Optional.absent() - : Optional.present(filter.location.country!), - state: filter.location.state == null ? const Optional.absent() : Optional.present(filter.location.state!), - city: filter.location.city == null ? const Optional.absent() : Optional.present(filter.location.city!), - make: filter.camera.make == null ? const Optional.absent() : Optional.present(filter.camera.make!), - model: filter.camera.model == null ? const Optional.absent() : Optional.present(filter.camera.model!), + : Optional.present(filter.location.country), + state: filter.location.state == null ? const Optional.absent() : Optional.present(filter.location.state), + city: filter.location.city == null ? const Optional.absent() : Optional.present(filter.location.city), + make: filter.camera.make == null ? const Optional.absent() : Optional.present(filter.camera.make), + model: filter.camera.model == null ? const Optional.absent() : Optional.present(filter.camera.model), takenAfter: filter.date.takenAfter == null ? const Optional.absent() - : Optional.present(filter.date.takenAfter!), + : Optional.present(filter.date.takenAfter), takenBefore: filter.date.takenBefore == null ? const Optional.absent() - : Optional.present(filter.date.takenBefore!), + : Optional.present(filter.date.takenBefore), visibility: Optional.present(filter.display.isArchive ? AssetVisibility.archive : AssetVisibility.timeline), rating: filter.rating.rating.toOptional(), isFavorite: filter.display.isFavorite ? const Optional.present(true) : const Optional.absent(), isNotInAlbum: filter.display.isNotInAlbum ? const Optional.present(true) : const Optional.absent(), personIds: Optional.present(filter.people.map((e) => e.id).toList()), - tagIds: filter.tagIds == null ? const Optional.absent() : Optional.present(filter.tagIds!), + tagIds: filter.tagIds == null ? const Optional.absent() : Optional.present(filter.tagIds), type: type == null ? const Optional.absent() : Optional.present(type), page: Optional.present(page), size: const Optional.present(100), @@ -53,29 +53,27 @@ class SearchApiRepository extends ApiRepository { return _api.searchAssets( MetadataSearchDto( originalFileName: filter.filename != null && filter.filename!.isNotEmpty - ? Optional.present(filter.filename!) + ? Optional.present(filter.filename) : const Optional.absent(), - country: filter.location.country == null ? const Optional.absent() : Optional.present(filter.location.country!), + country: filter.location.country == null ? const Optional.absent() : Optional.present(filter.location.country), description: filter.description != null && filter.description!.isNotEmpty - ? Optional.present(filter.description!) + ? Optional.present(filter.description) : const Optional.absent(), - ocr: filter.ocr != null && filter.ocr!.isNotEmpty ? Optional.present(filter.ocr!) : const Optional.absent(), - state: filter.location.state == null ? const Optional.absent() : Optional.present(filter.location.state!), - city: filter.location.city == null ? const Optional.absent() : Optional.present(filter.location.city!), - make: filter.camera.make == null ? const Optional.absent() : Optional.present(filter.camera.make!), - model: filter.camera.model == null ? const Optional.absent() : Optional.present(filter.camera.model!), - takenAfter: filter.date.takenAfter == null - ? const Optional.absent() - : Optional.present(filter.date.takenAfter!), + ocr: filter.ocr != null && filter.ocr!.isNotEmpty ? Optional.present(filter.ocr) : const Optional.absent(), + state: filter.location.state == null ? const Optional.absent() : Optional.present(filter.location.state), + city: filter.location.city == null ? const Optional.absent() : Optional.present(filter.location.city), + make: filter.camera.make == null ? const Optional.absent() : Optional.present(filter.camera.make), + model: filter.camera.model == null ? const Optional.absent() : Optional.present(filter.camera.model), + takenAfter: filter.date.takenAfter == null ? const Optional.absent() : Optional.present(filter.date.takenAfter), takenBefore: filter.date.takenBefore == null ? const Optional.absent() - : Optional.present(filter.date.takenBefore!), + : Optional.present(filter.date.takenBefore), visibility: Optional.present(filter.display.isArchive ? AssetVisibility.archive : AssetVisibility.timeline), rating: filter.rating.rating.toOptional(), isFavorite: filter.display.isFavorite ? const Optional.present(true) : const Optional.absent(), isNotInAlbum: filter.display.isNotInAlbum ? const Optional.present(true) : const Optional.absent(), personIds: Optional.present(filter.people.map((e) => e.id).toList()), - tagIds: filter.tagIds == null ? const Optional.absent() : Optional.present(filter.tagIds!), + tagIds: filter.tagIds == null ? const Optional.absent() : Optional.present(filter.tagIds), type: type == null ? const Optional.absent() : Optional.present(type), page: Optional.present(page), size: const Optional.present(1000), diff --git a/mobile/lib/infrastructure/repositories/sync_api.repository.dart b/mobile/lib/infrastructure/repositories/sync_api.repository.dart index e9d57f7506..303859da2f 100644 --- a/mobile/lib/infrastructure/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -80,7 +80,7 @@ class SyncApiRepository { ); String previousChunk = ''; - List lines = []; + final List lines = []; bool shouldAbort = false; @@ -105,7 +105,7 @@ class SyncApiRepository { } previousChunk += chunk; - final parts = previousChunk.toString().split('\n'); + final parts = previousChunk.split('\n'); previousChunk = parts.removeLast(); lines.addAll(parts); diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index bdfb1942ab..844226d49f 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -31,8 +31,8 @@ import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart' as api show AssetVisibility, AlbumUserRole, UserMetadataKey, AssetEditAction; -import 'package:openapi/api.dart' hide UserMetadataKey, AssetEditAction, AssetVisibility, AlbumUserRole; +import 'package:openapi/api.dart' as api show AlbumUserRole, AssetEditAction, AssetVisibility, UserMetadataKey; +import 'package:openapi/api.dart' hide AlbumUserRole, AssetEditAction, AssetVisibility, UserMetadataKey; class SyncStreamRepository extends DriftDatabaseRepository { final Logger _logger = Logger('DriftSyncStreamRepository'); @@ -287,8 +287,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { fNumber: Value(exif.fNumber), fileSize: Value(exif.fileSizeInByte), focalLength: Value(exif.focalLength), - latitude: Value(exif.latitude?.toDouble()), - longitude: Value(exif.longitude?.toDouble()), + latitude: Value(exif.latitude), + longitude: Value(exif.longitude), iso: Value(exif.iso), make: Value(exif.make), model: Value(exif.model), diff --git a/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart b/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart index 08712588d9..e31b47a9fc 100644 --- a/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart @@ -66,7 +66,7 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { return; } final assetIds = trashedAssets.map((e) => e.asset.id).toSet(); - Map localChecksumById = await _getCachedChecksums(assetIds); + final Map localChecksumById = await _getCachedChecksums(assetIds); return _db.transaction(() async { await _db.batch((batch) { diff --git a/mobile/lib/infrastructure/utils/exif.converter.dart b/mobile/lib/infrastructure/utils/exif.converter.dart index 9f9b6f9324..d47c9cc97a 100644 --- a/mobile/lib/infrastructure/utils/exif.converter.dart +++ b/mobile/lib/infrastructure/utils/exif.converter.dart @@ -21,7 +21,7 @@ abstract final class ExifDtoConverter { lens: dto.lensModel.orElse(null), f: dto.fNumber.orElse(null)?.toDouble(), mm: dto.focalLength.orElse(null)?.toDouble(), - iso: dto.iso.orElse(null)?.toInt(), + iso: dto.iso.orElse(null), exposureSeconds: exposureTimeToSeconds(dto.exposureTime.orElse(null)), ); } @@ -40,7 +40,7 @@ abstract final class ExifDtoConverter { if (second == null) { return null; } - double? value = double.tryParse(second); + final double? value = double.tryParse(second); if (value != null) { return value; } diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index f576a7c63c..09bcdc752f 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -24,13 +24,13 @@ import 'package:immich_mobile/pages/common/splash_screen.page.dart'; import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; -import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/theme.provider.dart'; +import 'package:immich_mobile/providers/view_intent/view_intent_handler.provider.dart'; import 'package:immich_mobile/routing/app_navigation_observer.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/deep_link.service.dart'; @@ -84,7 +84,7 @@ Future initApp() async { FlutterError.presentError(details); log.severe( 'FlutterError - Catch all', - "${details.toString()}\nException: ${details.exception}\nLibrary: ${details.library}\nContext: ${details.context}", + "$details\nException: ${details.exception}\nLibrary: ${details.library}\nContext: ${details.context}", details.stack, ); }; @@ -130,23 +130,18 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve dPrint(() => "[APP STATE] resumed"); ref.read(appStateProvider.notifier).handleAppResume(); unawaited(ref.read(viewIntentHandlerProvider).onAppResumed()); - break; case AppLifecycleState.inactive: dPrint(() => "[APP STATE] inactive"); ref.read(appStateProvider.notifier).handleAppInactivity(); - break; case AppLifecycleState.paused: dPrint(() => "[APP STATE] paused"); ref.read(appStateProvider.notifier).handleAppPause(); - break; case AppLifecycleState.detached: dPrint(() => "[APP STATE] detached"); ref.read(appStateProvider.notifier).handleAppDetached(); - break; case AppLifecycleState.hidden: dPrint(() => "[APP STATE] hidden"); ref.read(appStateProvider.notifier).handleAppHidden(); - break; } } @@ -219,7 +214,7 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve } @override - initState() { + void initState() { super.initState(); initApp().then((_) => dPrint(() => "App Init Completed")); WidgetsBinding.instance.addPostFrameCallback((_) { diff --git a/mobile/lib/pages/backup/drift_backup.page.dart b/mobile/lib/pages/backup/drift_backup.page.dart index 9e78fb4795..793437579a 100644 --- a/mobile/lib/pages/backup/drift_backup.page.dart +++ b/mobile/lib/pages/backup/drift_backup.page.dart @@ -67,7 +67,7 @@ class _DriftBackupPageState extends ConsumerState { } @override - dispose() { + void dispose() { super.dispose(); WakelockPlus.disable(); } diff --git a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart index b0616589f1..9f60a4e193 100644 --- a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart @@ -12,8 +12,8 @@ import 'package:immich_mobile/infrastructure/repositories/settings.repository.da import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart'; import 'package:immich_mobile/widgets/common/search_field.dart'; @@ -321,9 +321,9 @@ class _AlbumSelectionList extends StatelessWidget { return SliverPadding( padding: const EdgeInsets.symmetric(vertical: 12.0), sliver: SliverList( - delegate: SliverChildBuilderDelegate(((context, index) { + delegate: SliverChildBuilderDelegate((context, index) { return DriftAlbumInfoListTile(album: filteredAlbums[index]); - }), childCount: filteredAlbums.length), + }, childCount: filteredAlbums.length), ), ); } @@ -345,9 +345,9 @@ class _AlbumSelectionGrid extends StatelessWidget { crossAxisSpacing: 12, ), itemCount: filteredAlbums.length, - itemBuilder: ((context, index) { + itemBuilder: (context, index) { return DriftAlbumInfoListTile(album: filteredAlbums[index]); - }), + }, ), ); } diff --git a/mobile/lib/pages/backup/drift_backup_asset_detail.page.dart b/mobile/lib/pages/backup/drift_backup_asset_detail.page.dart index 36d51c5624..b35abd198e 100644 --- a/mobile/lib/pages/backup/drift_backup_asset_detail.page.dart +++ b/mobile/lib/pages/backup/drift_backup_asset_detail.page.dart @@ -20,7 +20,7 @@ class DriftBackupAssetDetailPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - AsyncValue> result = ref.watch(driftBackupCandidateProvider); + final AsyncValue> result = ref.watch(driftBackupCandidateProvider); return Scaffold( appBar: AppBar(title: Text('backup_controller_page_remainder'.t(context: context))), body: result.when( diff --git a/mobile/lib/pages/backup/drift_upload_detail.page.dart b/mobile/lib/pages/backup/drift_upload_detail.page.dart index 978fcf7c57..3c2570a434 100644 --- a/mobile/lib/pages/backup/drift_upload_detail.page.dart +++ b/mobile/lib/pages/backup/drift_upload_detail.page.dart @@ -520,7 +520,7 @@ class _CurrentUploadThumbnail extends ConsumerWidget { ), clipBehavior: Clip.antiAlias, child: snapshot.data != null - ? Thumbnail.fromAsset(asset: snapshot.data!, size: const Size(48, 48), fit: BoxFit.cover) + ? Thumbnail.fromAsset(asset: snapshot.data, size: const Size(48, 48), fit: BoxFit.cover) : Icon(Icons.image, size: 24, color: context.colorScheme.primary), ), ); diff --git a/mobile/lib/pages/common/app_log.page.dart b/mobile/lib/pages/common/app_log.page.dart index 336bf0b605..5458d90808 100644 --- a/mobile/lib/pages/common/app_log.page.dart +++ b/mobile/lib/pages/common/app_log.page.dart @@ -90,7 +90,7 @@ class AppLogPage extends HookConsumerWidget { }, itemCount: logMessages.data?.length ?? 0, itemBuilder: (context, index) { - var logMessage = logMessages.data![index]; + final logMessage = logMessages.data![index]; return ListTile( onTap: () => context.pushRoute(AppLogDetailRoute(logMessage: logMessage)), trailing: const Icon(Icons.arrow_forward_ios_rounded), @@ -116,7 +116,7 @@ class AppLogPage extends HookConsumerWidget { /// Truncate the log message to a certain number of lines /// @param int maxLines - Max number of lines to truncate String truncateLogMessage(String message, int maxLines) { - List messageLines = message.split("\n"); + final List messageLines = message.split("\n"); if (messageLines.length < maxLines) { return message; } diff --git a/mobile/lib/pages/common/app_log_detail.page.dart b/mobile/lib/pages/common/app_log_detail.page.dart index 890e46888f..274231a729 100644 --- a/mobile/lib/pages/common/app_log_detail.page.dart +++ b/mobile/lib/pages/common/app_log_detail.page.dart @@ -14,7 +14,7 @@ class AppLogDetailPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - buildTextWithCopyButton(String header, String text) { + Padding buildTextWithCopyButton(String header, String text) { return Padding( padding: const EdgeInsets.all(8.0), child: Column( @@ -66,7 +66,7 @@ class AppLogDetailPage extends HookConsumerWidget { ); } - buildLogContext(String logger) { + Padding buildLogContext(String logger) { return Padding( padding: const EdgeInsets.all(8.0), child: Column( @@ -87,7 +87,7 @@ class AppLogDetailPage extends HookConsumerWidget { child: Padding( padding: const EdgeInsets.all(8.0), child: SelectableText( - logger.toString(), + logger, style: const TextStyle(fontSize: 12.0, fontWeight: FontWeight.bold, fontFamily: "GoogleSansCode"), ), ), diff --git a/mobile/lib/pages/common/download_panel.dart b/mobile/lib/pages/common/download_panel.dart index 0775f5b4e4..f39aa07166 100644 --- a/mobile/lib/pages/common/download_panel.dart +++ b/mobile/lib/pages/common/download_panel.dart @@ -14,7 +14,7 @@ class DownloadPanel extends ConsumerWidget { final tasks = ref.watch(downloadStateProvider.select((state) => state.taskProgress)).entries.toList(); - onCancelDownload(String id) { + void onCancelDownload(String id) { ref.watch(downloadStateProvider.notifier).cancelDownload(id); } diff --git a/mobile/lib/pages/common/headers_settings.page.dart b/mobile/lib/pages/common/headers_settings.page.dart index 9a6b602b04..f0b3f4b67f 100644 --- a/mobile/lib/pages/common/headers_settings.page.dart +++ b/mobile/lib/pages/common/headers_settings.page.dart @@ -41,7 +41,7 @@ class HeaderSettingsPage extends HookConsumerWidget { } setInitialHeaders.value = true; - var list = [ + final list = [ ...headers.value.map((headerValue) { return HeaderKeyValueSettings( header: headerValue, @@ -81,7 +81,7 @@ class HeaderSettingsPage extends HookConsumerWidget { ); } - saveHeaders(WidgetRef ref, List headers) async { + Future saveHeaders(WidgetRef ref, List headers) async { final headersMap = {}; for (final header in headers) { final key = header.key.trim(); diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index 0d423875cb..711783bc94 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -25,7 +25,7 @@ import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/widgets/common/immich_logo.dart'; import 'package:immich_mobile/widgets/common/immich_title_text.dart'; import 'package:logging/logging.dart'; -import 'package:url_launcher/url_launcher.dart' show launchUrl, LaunchMode; +import 'package:url_launcher/url_launcher.dart' show LaunchMode, launchUrl; class BootstrapErrorWidget extends StatelessWidget { final String error; @@ -297,7 +297,7 @@ class SplashScreenPageState extends ConsumerState { log.info("Resuming session at $endpoint"); } - void resumeSession() async { + Future resumeSession() async { final serverUrl = Store.tryGet(StoreKey.serverUrl); final endpoint = Store.tryGet(StoreKey.serverEndpoint); final accessToken = Store.tryGet(StoreKey.accessToken); diff --git a/mobile/lib/pages/library/folder/folder.page.dart b/mobile/lib/pages/library/folder/folder.page.dart index 5efb5ccc62..6934d7b6c5 100644 --- a/mobile/lib/pages/library/folder/folder.page.dart +++ b/mobile/lib/pages/library/folder/folder.page.dart @@ -89,7 +89,7 @@ class FolderPage extends HookConsumerWidget { if (folder == null) { return FolderContent(folder: rootFolder, root: rootFolder, sortOrder: sortOrder.value); } else { - return FolderContent(folder: currentFolder.value!, root: rootFolder, sortOrder: sortOrder.value); + return FolderContent(folder: currentFolder.value, root: rootFolder, sortOrder: sortOrder.value); } }, loading: () => const Center(child: CircularProgressIndicator()), @@ -126,7 +126,7 @@ class FolderContent extends HookConsumerWidget { return Center(child: const Text("folder_not_found").tr()); } - getSubtitle(int subFolderCount) { + String getSubtitle(int subFolderCount) { if (subFolderCount > 0) { return "$subFolderCount ${tr("folders")}".toLowerCase(); } diff --git a/mobile/lib/pages/library/locked/pin_auth.page.dart b/mobile/lib/pages/library/locked/pin_auth.page.dart index 3af320dc5f..7beda1d47b 100644 --- a/mobile/lib/pages/library/locked/pin_auth.page.dart +++ b/mobile/lib/pages/library/locked/pin_auth.page.dart @@ -38,7 +38,7 @@ class PinAuthPage extends HookConsumerWidget { } } - enableBiometricAuth() { + void enableBiometricAuth() { showDialog( context: context, builder: (buildContext) { diff --git a/mobile/lib/pages/login/login.page.dart b/mobile/lib/pages/login/login.page.dart index 5f40b32baa..79091d2679 100644 --- a/mobile/lib/pages/login/login.page.dart +++ b/mobile/lib/pages/login/login.page.dart @@ -4,8 +4,8 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/widgets/forms/login/login_form.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/widgets/forms/login/login_form.dart'; import 'package:package_info_plus/package_info_plus.dart'; @RoutePage() @@ -16,8 +16,8 @@ class LoginPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final appVersion = useState('0.0.0'); - getAppInfo() async { - PackageInfo packageInfo = await PackageInfo.fromPlatform(); + Future getAppInfo() async { + final PackageInfo packageInfo = await PackageInfo.fromPlatform(); appVersion.value = packageInfo.version; } diff --git a/mobile/lib/pages/search/map/map_location_picker.page.dart b/mobile/lib/pages/search/map/map_location_picker.page.dart index 3dace15ced..96f41a4d38 100644 --- a/mobile/lib/pages/search/map/map_location_picker.page.dart +++ b/mobile/lib/pages/search/map/map_location_picker.page.dart @@ -41,13 +41,13 @@ class MapLocationPickerPage extends HookConsumerWidget { } Future getCurrentLocation() async { - var (currentLocation, _) = await MapUtils.checkPermAndGetLocation(context: context); + final (currentLocation, _) = await MapUtils.checkPermAndGetLocation(context: context); if (currentLocation == null) { return; } - var currentLatLng = LatLng(currentLocation.latitude, currentLocation.longitude); + final currentLatLng = LatLng(currentLocation.latitude, currentLocation.longitude); selectedLatLng.value = currentLatLng; await controller.value?.animateCamera(CameraUpdate.newLatLngZoom(currentLatLng, 12)); } diff --git a/mobile/lib/pages/share_intent/share_intent.page.dart b/mobile/lib/pages/share_intent/share_intent.page.dart index 2744b187de..ec88c4a9e4 100644 --- a/mobile/lib/pages/share_intent/share_intent.page.dart +++ b/mobile/lib/pages/share_intent/share_intent.page.dart @@ -35,7 +35,7 @@ class ShareIntentPage extends ConsumerWidget { ref.read(shareIntentUploadProvider.notifier).addAttachments(attachments); } - void upload() async { + Future upload() async { final files = candidates.map((candidate) => candidate.file).toList(); await ref.read(shareIntentUploadProvider.notifier).uploadAll(files); } @@ -102,7 +102,7 @@ class ShareIntentPage extends ConsumerWidget { Icons.image, color: Colors.white, size: 20, - shadows: [Shadow(offset: Offset(0, 0), blurRadius: 8.0, color: Colors.black45)], + shadows: [Shadow(offset: Offset.zero, blurRadius: 8.0, color: Colors.black45)], ), ), ], diff --git a/mobile/lib/presentation/pages/dev/main_timeline.page.dart b/mobile/lib/presentation/pages/dev/main_timeline.page.dart index b78bbc2979..f2215d169c 100644 --- a/mobile/lib/presentation/pages/dev/main_timeline.page.dart +++ b/mobile/lib/presentation/pages/dev/main_timeline.page.dart @@ -1,9 +1,9 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/presentation/widgets/feature_message/feature_message_dialog.widget.dart'; import 'package:immich_mobile/presentation/widgets/memory/memory_lane.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; -import 'package:immich_mobile/presentation/widgets/feature_message/feature_message_dialog.widget.dart'; import 'package:immich_mobile/providers/feature_message.provider.dart'; import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; diff --git a/mobile/lib/presentation/pages/download_info.page.dart b/mobile/lib/presentation/pages/download_info.page.dart index e805458e76..af44714b83 100644 --- a/mobile/lib/presentation/pages/download_info.page.dart +++ b/mobile/lib/presentation/pages/download_info.page.dart @@ -14,14 +14,14 @@ class DownloadInfoPage extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final tasks = ref.watch(downloadStateProvider.select((state) => state.taskProgress)).entries.toList(); - onCancelDownload(String id) { + void onCancelDownload(String id) { ref.watch(downloadStateProvider.notifier).cancelDownload(id); } return Scaffold( appBar: AppBar( title: Text("download".t(context: context)), - actions: [], + actions: const [], ), body: ListView.builder( physics: const ClampingScrollPhysics(), diff --git a/mobile/lib/presentation/pages/drift_activities.page.dart b/mobile/lib/presentation/pages/drift_activities.page.dart index b998e10dc2..a52f1d7358 100644 --- a/mobile/lib/presentation/pages/drift_activities.page.dart +++ b/mobile/lib/presentation/pages/drift_activities.page.dart @@ -45,7 +45,7 @@ class DriftActivitiesPage extends HookConsumerWidget { if (assetName != null) Text(assetName!, style: context.textTheme.bodySmall), ], ), - actions: [const LikeActivityActionButton(iconOnly: true)], + actions: const [LikeActivityActionButton(iconOnly: true)], actionsPadding: const EdgeInsets.only(right: 8), ), body: activities.widgetWhen( diff --git a/mobile/lib/presentation/pages/drift_album_options.page.dart b/mobile/lib/presentation/pages/drift_album_options.page.dart index 1a516426b5..84060aa38c 100644 --- a/mobile/lib/presentation/pages/drift_album_options.page.dart +++ b/mobile/lib/presentation/pages/drift_album_options.page.dart @@ -43,7 +43,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { ); } - void leaveAlbum() async { + Future leaveAlbum() async { try { await ref.read(remoteAlbumProvider.notifier).leaveAlbum(album.id, userId: userId); unawaited(context.navigateTo(const DriftAlbumsRoute())); @@ -52,7 +52,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { } } - void removeUserFromAlbum(UserDto user) async { + Future removeUserFromAlbum(UserDto user) async { try { await ref.read(remoteAlbumProvider.notifier).removeUser(album.id, user.id); ref.invalidate(remoteAlbumSharedUsersProvider(album.id)); @@ -83,11 +83,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { ref.invalidate(remoteAlbumSharedUsersProvider(album.id)); } catch (e) { - ImmichToast.show( - context: context, - msg: "Failed to add users to album: ${e.toString()}", - toastType: ToastType.error, - ); + ImmichToast.show(context: context, msg: "Failed to add users to album: $e", toastType: ToastType.error); } } @@ -129,7 +125,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { ); } - buildOwnerInfo() { + Widget buildOwnerInfo() { if (isOwner) { final owner = ref.watch(currentUserProvider); return ListTile( @@ -160,7 +156,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { } } - buildSharedUsersList() { + Widget buildSharedUsersList() { return sharedUsersAsync.maybeWhen( data: (sharedUsers) => ListView.builder( primary: false, @@ -181,7 +177,7 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { ); } - buildSectionTitle(String text) { + Padding buildSectionTitle(String text) { return Padding( padding: const EdgeInsets.all(16.0), child: Text(text, style: context.textTheme.bodySmall), diff --git a/mobile/lib/presentation/pages/drift_library.page.dart b/mobile/lib/presentation/pages/drift_library.page.dart index e93a58be7d..190ad3af6a 100644 --- a/mobile/lib/presentation/pages/drift_library.page.dart +++ b/mobile/lib/presentation/pages/drift_library.page.dart @@ -370,7 +370,7 @@ class _QuickAccessButtonList extends ConsumerWidget { ), child: ListView( shrinkWrap: true, - padding: const EdgeInsets.all(0), + padding: EdgeInsets.zero, physics: const NeverScrollableScrollPhysics(), children: [ ListTile( @@ -422,7 +422,7 @@ class _PartnerList extends StatelessWidget { @override Widget build(BuildContext context) { return ListView.builder( - padding: const EdgeInsets.all(0), + padding: EdgeInsets.zero, physics: const NeverScrollableScrollPhysics(), itemCount: partners.length, shrinkWrap: true, diff --git a/mobile/lib/presentation/pages/drift_memory.page.dart b/mobile/lib/presentation/pages/drift_memory.page.dart index f601bf8419..4ae97f30e1 100644 --- a/mobile/lib/presentation/pages/drift_memory.page.dart +++ b/mobile/lib/presentation/pages/drift_memory.page.dart @@ -7,10 +7,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/memory/memory_bottom_info.widget.dart'; import 'package:immich_mobile/presentation/widgets/memory/memory_card.widget.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/utils/system_ui.utils.dart'; import 'package:immich_mobile/widgets/memories/memory_epilogue.dart'; @@ -54,7 +54,7 @@ class DriftMemoryPage extends HookConsumerWidget { }; }); - toNextMemory() { + void toNextMemory() { memoryPageController.nextPage(duration: const Duration(milliseconds: 500), curve: Curves.easeIn); } @@ -83,10 +83,10 @@ class DriftMemoryPage extends HookConsumerWidget { } } - toNextAsset(int currentAssetIndex) { + void toNextAsset(int currentAssetIndex) { if (currentAssetIndex + 1 < currentMemory.value.assets.length) { // Go to the next asset - PageController controller = memoryAssetPageControllers[currentMemoryIndex.value]; + final PageController controller = memoryAssetPageControllers[currentMemoryIndex.value]; controller.nextPage(curve: Curves.easeInOut, duration: const Duration(milliseconds: 500)); } else { @@ -95,10 +95,10 @@ class DriftMemoryPage extends HookConsumerWidget { } } - toPreviousAsset(int currentAssetIndex) { + void toPreviousAsset(int currentAssetIndex) { if (currentAssetIndex > 0) { // Go to the previous asset - PageController controller = memoryAssetPageControllers[currentMemoryIndex.value]; + final PageController controller = memoryAssetPageControllers[currentMemoryIndex.value]; controller.previousPage(curve: Curves.easeInOut, duration: const Duration(milliseconds: 500)); } else { @@ -107,12 +107,12 @@ class DriftMemoryPage extends HookConsumerWidget { } } - updateProgressText() { + void updateProgressText() { assetProgress.value = "${currentAssetPage.value + 1}|${currentMemory.value.assets.length}"; } /// Downloads and caches the image for the asset at this [currentMemory]'s index - precacheAsset(int index) async { + Future precacheAsset(int index) async { // Guard index out of range if (index < 0) { return; diff --git a/mobile/lib/presentation/pages/drift_partner_detail.page.dart b/mobile/lib/presentation/pages/drift_partner_detail.page.dart index 7df96cf78e..fd5b64c108 100644 --- a/mobile/lib/presentation/pages/drift_partner_detail.page.dart +++ b/mobile/lib/presentation/pages/drift_partner_detail.page.dart @@ -56,7 +56,7 @@ class _InfoBoxState extends ConsumerState<_InfoBox> { _inTimeline = widget.partner.inTimeline; } - _toggleInTimeline() async { + Future _toggleInTimeline() async { final user = ref.read(currentUserProvider); if (user == null) { return; diff --git a/mobile/lib/presentation/pages/drift_people_collection.page.dart b/mobile/lib/presentation/pages/drift_people_collection.page.dart index 0afe723dc6..f39b5e15c7 100644 --- a/mobile/lib/presentation/pages/drift_people_collection.page.dart +++ b/mobile/lib/presentation/pages/drift_people_collection.page.dart @@ -4,8 +4,8 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/string_extensions.dart'; -import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; +import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:immich_mobile/utils/people.utils.dart'; diff --git a/mobile/lib/presentation/pages/drift_person.page.dart b/mobile/lib/presentation/pages/drift_person.page.dart index 3430dd1abd..d4c280d010 100644 --- a/mobile/lib/presentation/pages/drift_person.page.dart +++ b/mobile/lib/presentation/pages/drift_person.page.dart @@ -24,7 +24,7 @@ class _DriftPersonPageState extends ConsumerState { late DriftPerson _person; @override - initState() { + void initState() { super.initState(); _person = widget.person; } diff --git a/mobile/lib/presentation/pages/drift_remote_album.page.dart b/mobile/lib/presentation/pages/drift_remote_album.page.dart index ccbddb99f3..5e4e525e06 100644 --- a/mobile/lib/presentation/pages/drift_remote_album.page.dart +++ b/mobile/lib/presentation/pages/drift_remote_album.page.dart @@ -82,11 +82,7 @@ class _RemoteAlbumPageState extends ConsumerState { ref.invalidate(remoteAlbumSharedUsersProvider(_album.id)); } catch (e) { - ImmichToast.show( - context: context, - msg: "Failed to add users to album: ${e.toString()}", - toastType: ToastType.error, - ); + ImmichToast.show(context: context, msg: "Failed to add users to album: $e", toastType: ToastType.error); } } diff --git a/mobile/lib/presentation/pages/drift_slideshow.page.dart b/mobile/lib/presentation/pages/drift_slideshow.page.dart index 260ed3ba78..3f0c441c01 100644 --- a/mobile/lib/presentation/pages/drift_slideshow.page.dart +++ b/mobile/lib/presentation/pages/drift_slideshow.page.dart @@ -54,7 +54,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si bool _disableAnimations = false; @override - initState() { + void initState() { super.initState(); _config = ref.read(appConfigProvider.select((s) => s.slideshow)); final asset = ref.read(assetViewerProvider).currentAsset; @@ -78,7 +78,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si } @override - dispose() { + void dispose() { _timer.cancel(); _stopwatch.stop(); _pageController.dispose(); @@ -151,7 +151,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si } } - void _nextPage() async { + Future _nextPage() async { if (_nextIndex < 0 || _nextIndex >= widget.timeline.totalAssets) { if (_config.repeat) { final wrapped = _config.direction == SlideshowDirection.forward ? 0 : widget.timeline.totalAssets - 1; @@ -267,7 +267,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si _updateNextIndex(); } - void _onTapUp() async { + Future _onTapUp() async { await (_showAppBar ? SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive) : restoreEdgeToEdge()); WidgetsBinding.instance.addPostFrameCallback((_) { @@ -295,7 +295,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si } else { return LinearProgressIndicator( color: context.colorScheme.primary, - borderRadius: const BorderRadius.all(Radius.zero), + borderRadius: BorderRadius.zero, minHeight: 5, value: ref.watch(videoPlayerProvider(asset.heroTag).select((s) => s.position)).inMilliseconds / @@ -539,7 +539,7 @@ class _SlideshowProgressBarState extends State<_SlideshowProgressBar> with Singl animation: _controller, builder: (context, _) => LinearProgressIndicator( color: widget.color, - borderRadius: const BorderRadius.all(Radius.zero), + borderRadius: BorderRadius.zero, minHeight: 5, value: _controller.value, ), diff --git a/mobile/lib/presentation/pages/drift_trash.page.dart b/mobile/lib/presentation/pages/drift_trash.page.dart index d21b437efe..36db74a658 100644 --- a/mobile/lib/presentation/pages/drift_trash.page.dart +++ b/mobile/lib/presentation/pages/drift_trash.page.dart @@ -6,8 +6,8 @@ import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -41,7 +41,7 @@ class DriftTrashPage extends StatelessWidget { pinned: true, centerTitle: true, elevation: 0, - actions: [const _TrashKebabMenu()], + actions: const [_TrashKebabMenu()], ), topSliverWidgetHeight: 24, topSliverWidget: Consumer( diff --git a/mobile/lib/presentation/pages/drift_user_selection.page.dart b/mobile/lib/presentation/pages/drift_user_selection.page.dart index b73913fd02..41394014a0 100644 --- a/mobile/lib/presentation/pages/drift_user_selection.page.dart +++ b/mobile/lib/presentation/pages/drift_user_selection.page.dart @@ -52,11 +52,11 @@ class DriftUserSelectionPage extends HookConsumerWidget { final AsyncValue> suggestedShareUsers = ref.watch(driftUsersProvider); final sharedUsersList = useState>({}); - addNewUsersHandler() { + void addNewUsersHandler() { context.maybePop(sharedUsersList.value.map((e) => e.id).toList()); } - buildTileIcon(UserDto user) { + Widget buildTileIcon(UserDto user) { if (sharedUsersList.value.contains(user)) { return CircleAvatar(backgroundColor: context.primaryColor, child: const Icon(Icons.check_rounded, size: 25)); } else { @@ -64,8 +64,8 @@ class DriftUserSelectionPage extends HookConsumerWidget { } } - buildUserList(List users) { - List usersChip = []; + ListView buildUserList(List users) { + final List usersChip = []; for (var user in sharedUsersList.value) { usersChip.add( @@ -91,7 +91,7 @@ class DriftUserSelectionPage extends HookConsumerWidget { ListView.builder( primary: false, shrinkWrap: true, - itemBuilder: ((context, index) { + itemBuilder: (context, index) { return ListTile( leading: buildTileIcon(users[index]), dense: true, @@ -107,7 +107,7 @@ class DriftUserSelectionPage extends HookConsumerWidget { } }, ); - }), + }, itemCount: users.length, ), ], diff --git a/mobile/lib/presentation/pages/edit/drift_edit.page.dart b/mobile/lib/presentation/pages/edit/drift_edit.page.dart index 2e2d39e386..0ce9985c19 100644 --- a/mobile/lib/presentation/pages/edit/drift_edit.page.dart +++ b/mobile/lib/presentation/pages/edit/drift_edit.page.dart @@ -15,7 +15,7 @@ import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/utils/editor.utils.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_ui/immich_ui.dart'; -import 'package:openapi/api.dart' show RotateParameters, MirrorParameters, MirrorAxis; +import 'package:openapi/api.dart' show MirrorAxis, MirrorParameters, RotateParameters; @RoutePage() class DriftEditImagePage extends ConsumerStatefulWidget { diff --git a/mobile/lib/presentation/pages/edit/editor.provider.dart b/mobile/lib/presentation/pages/edit/editor.provider.dart index 69c8b7bf54..3d97f2173f 100644 --- a/mobile/lib/presentation/pages/edit/editor.provider.dart +++ b/mobile/lib/presentation/pages/edit/editor.provider.dart @@ -25,7 +25,7 @@ class EditorProvider extends Notifier { final originalWidth = exifInfo.isFlipped ? exifInfo.height : exifInfo.width; final originalHeight = exifInfo.isFlipped ? exifInfo.width : exifInfo.height; - Rect crop = existingCrop != null && originalWidth != null && originalHeight != null + final Rect crop = existingCrop != null && originalWidth != null && originalHeight != null ? convertCropParametersToRect(existingCrop.parameters, originalWidth, originalHeight) : const Rect.fromLTRB(0, 0, 1, 1); diff --git a/mobile/lib/presentation/pages/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/drift_search.page.dart index 22546b8e50..6b818bd273 100644 --- a/mobile/lib/presentation/pages/search/drift_search.page.dart +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -80,7 +80,7 @@ class DriftSearchPage extends HookConsumerWidget { final userPreferences = ref.watch(userMetadataPreferencesProvider); - search(SearchFilter f) { + void search(SearchFilter f) { if (f == filter.value) { return; } @@ -94,7 +94,7 @@ class DriftSearchPage extends HookConsumerWidget { } } - loadMoreSearchResults() { + void loadMoreSearchResults() { unawaited(ref.read(paginatedSearchProvider.notifier).search(filter.value)); } @@ -123,19 +123,19 @@ class DriftSearchPage extends HookConsumerWidget { return null; }, [preFilter]); - showPeoplePicker() { + void showPeoplePicker() { var people = filter.value.people; - handleOnSelect(Set value) { + void handleOnSelect(Set value) { people = value; } - handleClear() { + void handleClear() { peopleCurrentFilterWidget.value = null; search(filter.value.copyWith(people: {})); } - handleApply() { + void handleApply() { final label = people.map((e) => e.name != '' ? e.name : 'no_name'.t(context: context)).join(', '); peopleCurrentFilterWidget.value = label.isNotEmpty ? Text(label, style: context.textTheme.labelLarge) : null; search(filter.value.copyWith(people: people)); @@ -157,21 +157,21 @@ class DriftSearchPage extends HookConsumerWidget { ); } - showTagPicker() { + void showTagPicker() { var tagIds = filter.value.tagIds ?? []; String tagLabel = ''; - handleOnSelect(Iterable tags) { + void handleOnSelect(Iterable tags) { tagIds = tags.map((t) => t.id).toList(); tagLabel = tags.map((t) => t.value).join(', '); } - handleClear() { + void handleClear() { tagCurrentFilterWidget.value = null; search(filter.value.copyWith(tagIds: [])); } - handleApply() { + void handleApply() { tagCurrentFilterWidget.value = tagLabel.isNotEmpty ? Text(tagLabel, style: context.textTheme.labelLarge) : null; search(filter.value.copyWith(tagIds: tagIds)); } @@ -192,19 +192,19 @@ class DriftSearchPage extends HookConsumerWidget { ); } - showLocationPicker() { + void showLocationPicker() { var location = filter.value.location; - handleOnSelect(Map value) { + void handleOnSelect(Map value) { location = SearchLocationFilter(country: value['country'], city: value['city'], state: value['state']); } - handleClear() { + void handleClear() { locationCurrentFilterWidget.value = null; search(filter.value.copyWith(location: SearchLocationFilter())); } - handleApply() { + void handleApply() { final locationText = [ if (location.country != null) location.country!, if (location.state != null) location.state!, @@ -238,19 +238,19 @@ class DriftSearchPage extends HookConsumerWidget { ); } - showCameraPicker() { + void showCameraPicker() { var camera = filter.value.camera; - handleOnSelect(Map value) { + void handleOnSelect(Map value) { camera = SearchCameraFilter(make: value['make'], model: value['model']); } - handleClear() { + void handleClear() { cameraCurrentFilterWidget.value = null; search(filter.value.copyWith(camera: SearchCameraFilter())); } - handleApply() { + void handleApply() { final make = camera.make ?? ''; final model = camera.model ?? ''; cameraCurrentFilterWidget.value = (make.isNotEmpty || model.isNotEmpty) @@ -275,7 +275,7 @@ class DriftSearchPage extends HookConsumerWidget { ); } - datePicked(DateFilterInputModel? selectedDate) { + void datePicked(DateFilterInputModel? selectedDate) { dateInputFilter.value = selectedDate; if (selectedDate == null) { dateRangeCurrentFilterWidget.value = null; @@ -298,7 +298,7 @@ class DriftSearchPage extends HookConsumerWidget { ); } - showDatePicker() async { + Future showDatePicker() async { final firstDate = DateTime(1900); final lastDate = DateTime.now(); @@ -338,7 +338,7 @@ class DriftSearchPage extends HookConsumerWidget { } } - showQuickDatePicker() { + void showQuickDatePicker() { showFilterBottomSheet( context: context, child: FilterBottomSheetScaffold( @@ -361,19 +361,19 @@ class DriftSearchPage extends HookConsumerWidget { } // MEDIA PICKER - showMediaTypePicker() { + void showMediaTypePicker() { var mediaType = filter.value.mediaType; - handleOnSelected(AssetType assetType) { + void handleOnSelected(AssetType assetType) { mediaType = assetType; } - handleClear() { + void handleClear() { mediaTypeCurrentFilterWidget.value = null; search(filter.value.copyWith(mediaType: AssetType.other)); } - handleApply() { + void handleApply() { mediaTypeCurrentFilterWidget.value = mediaType != AssetType.other ? Text( mediaType == AssetType.image ? 'image'.t(context: context) : 'video'.t(context: context), @@ -395,19 +395,19 @@ class DriftSearchPage extends HookConsumerWidget { } // STAR RATING PICKER - showStarRatingPicker() { + void showStarRatingPicker() { var rating = filter.value.rating; - handleOnSelected(SearchRatingFilter value) { + void handleOnSelected(SearchRatingFilter value) { rating = value; } - handleClear() { + void handleClear() { ratingCurrentFilterWidget.value = null; search(filter.value.copyWith(rating: SearchRatingFilter())); } - handleApply() { + void handleApply() { ratingCurrentFilterWidget.value = rating.rating.isSome ? Text( 'rating_count'.t(args: {'count': rating.rating.unwrapOrNull ?? 0}), @@ -430,10 +430,10 @@ class DriftSearchPage extends HookConsumerWidget { } // DISPLAY OPTION - showDisplayOptionPicker() { + void showDisplayOptionPicker() { var display = filter.value.display; - handleOnSelect(Map value) { + void handleOnSelect(Map value) { display = display.copyWith( isNotInAlbum: value[DisplayOption.notInAlbum], isArchive: value[DisplayOption.archive], @@ -441,7 +441,7 @@ class DriftSearchPage extends HookConsumerWidget { ); } - handleClear() { + void handleClear() { displayOptionCurrentFilterWidget.value = null; search( filter.value.copyWith( @@ -450,7 +450,7 @@ class DriftSearchPage extends HookConsumerWidget { ); } - handleApply() { + void handleApply() { final filterText = [ if (display.isNotInAlbum) 'search_filter_display_option_not_in_album'.t(context: context), if (display.isArchive) 'archive'.t(context: context), @@ -473,7 +473,7 @@ class DriftSearchPage extends HookConsumerWidget { ); } - handleTextSubmitted(String value) => search(switch (textSearchType.value) { + void handleTextSubmitted(String value) => search(switch (textSearchType.value) { TextSearchType.context => filter.value.copyWith(filename: '', context: value, description: '', ocr: ''), TextSearchType.filename => filter.value.copyWith(filename: value, context: '', description: '', ocr: ''), TextSearchType.description => filter.value.copyWith(filename: '', context: '', description: value, ocr: ''), diff --git a/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart index dc48ed57ec..86d3fa0749 100644 --- a/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart @@ -1,25 +1,23 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; +import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/providers/user.provider.dart'; - -import 'package:immich_mobile/domain/models/album/album.model.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; - -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; enum AddToMenuItem { album, archive, unarchive, lockedFolder } @@ -37,16 +35,12 @@ class _AddActionButtonState extends ConsumerState { switch (selected) { case AddToMenuItem.album: _openAlbumSelector(); - break; case AddToMenuItem.archive: performArchiveAction(context, ref, source: ActionSource.viewer); - break; case AddToMenuItem.unarchive: performUnArchiveAction(context, ref, source: ActionSource.viewer); - break; case AddToMenuItem.lockedFolder: performMoveToLockFolderAction(context, ref, source: ActionSource.viewer); - break; } } diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart index 2a7c4ba9d7..45dc5ec699 100644 --- a/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart @@ -30,7 +30,7 @@ class DeleteActionButton extends ConsumerWidget { this.menuItem = false, }); - void _onTap(BuildContext context, WidgetRef ref) async { + Future _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { return; } diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart index 6911d09f89..5a94d9807e 100644 --- a/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart @@ -7,9 +7,9 @@ import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; /// This delete action has the following behavior: /// - Prompt to delete the asset locally @@ -20,7 +20,7 @@ class DeleteLocalActionButton extends ConsumerWidget { const DeleteLocalActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - void _onTap(BuildContext context, WidgetRef ref) async { + Future _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { return; } diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart index 267a9f55e6..922f8593fa 100644 --- a/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart @@ -28,7 +28,7 @@ class DeletePermanentActionButton extends ConsumerWidget { this.useShortLabel = false, }); - void _onTap(BuildContext context, WidgetRef ref) async { + Future _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { return; } diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart index d19a188561..f3e048f06f 100644 --- a/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart @@ -18,7 +18,7 @@ class DeleteTrashActionButton extends ConsumerWidget { const DeleteTrashActionButton({super.key, required this.source}); - void _onTap(BuildContext context, WidgetRef ref) async { + Future _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { return; } diff --git a/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart index a5129b643a..b6f8cc614e 100644 --- a/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart @@ -1,6 +1,6 @@ -import 'package:immich_mobile/constants/enums.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; @@ -14,7 +14,7 @@ class DownloadActionButton extends ConsumerWidget { final bool menuItem; const DownloadActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - void _onTap(BuildContext context, WidgetRef ref, BackgroundSyncManager backgroundSyncManager) async { + Future _onTap(BuildContext context, WidgetRef ref, BackgroundSyncManager backgroundSyncManager) async { if (!context.mounted) { return; } diff --git a/mobile/lib/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart index 6eeec0658b..b2b5050a8e 100644 --- a/mobile/lib/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart @@ -13,7 +13,7 @@ class EditDateTimeActionButton extends ConsumerWidget { const EditDateTimeActionButton({super.key, required this.source}); - _onTap(BuildContext context, WidgetRef ref) async { + Future _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { return; } diff --git a/mobile/lib/presentation/widgets/action_buttons/edit_location_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/edit_location_action_button.widget.dart index 1a8a1a5c39..cc8e15617c 100644 --- a/mobile/lib/presentation/widgets/action_buttons/edit_location_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/edit_location_action_button.widget.dart @@ -13,7 +13,7 @@ class EditLocationActionButton extends ConsumerWidget { const EditLocationActionButton({super.key, required this.source}); - _onTap(BuildContext context, WidgetRef ref) async { + Future _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { return; } diff --git a/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart index 07ace7e631..0365335fd2 100644 --- a/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart @@ -17,7 +17,7 @@ class FavoriteActionButton extends ConsumerWidget { const FavoriteActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - void _onTap(BuildContext context, WidgetRef ref) async { + Future _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { return; } diff --git a/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart index 4cb973cca1..46a5c2c41e 100644 --- a/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/like_activity_action_button.widget.dart @@ -25,7 +25,7 @@ class LikeActivityActionButton extends ConsumerWidget { final activities = ref.watch(albumActivityProvider((album?.id ?? "", asset?.id))); - onTap(Activity? liked) async { + Future onTap(Activity? liked) async { if (user == null) { return; } diff --git a/mobile/lib/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart index 541a9f8093..adf73e4107 100644 --- a/mobile/lib/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart @@ -21,20 +21,17 @@ class OpenInBrowserActionButton extends ConsumerWidget { this.menuItem = false, }); - void _onTap() async { + Future _onTap() async { final serverEndpoint = Store.get(StoreKey.serverEndpoint).replaceFirst('/api', ''); String originPath = ''; switch (origin) { case TimelineOrigin.favorite: originPath = '/favorites'; - break; case TimelineOrigin.trash: originPath = '/trash'; - break; case TimelineOrigin.archive: originPath = '/archive'; - break; default: break; } diff --git a/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart index 97a36a56dc..ebcfbaa1e5 100644 --- a/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart @@ -24,7 +24,7 @@ class RemoveFromAlbumActionButton extends ConsumerWidget { this.menuItem = false, }); - void _onTap(BuildContext context, WidgetRef ref) async { + Future _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { return; } diff --git a/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart index 17d2a76af7..75deef9ccb 100644 --- a/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart @@ -20,7 +20,7 @@ class RemoveFromLockFolderActionButton extends ConsumerWidget { this.menuItem = false, }); - void _onTap(BuildContext context, WidgetRef ref) async { + Future _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { return; } diff --git a/mobile/lib/presentation/widgets/action_buttons/restore_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/restore_action_button.widget.dart index 1713718967..b752a77c89 100644 --- a/mobile/lib/presentation/widgets/action_buttons/restore_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/restore_action_button.widget.dart @@ -17,7 +17,7 @@ class RestoreActionButton extends ConsumerWidget { const RestoreActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - void _onTap(BuildContext context, WidgetRef ref) async { + Future _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { return; } diff --git a/mobile/lib/presentation/widgets/action_buttons/restore_trash_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/restore_trash_action_button.widget.dart index e7928bd325..82a9d98549 100644 --- a/mobile/lib/presentation/widgets/action_buttons/restore_trash_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/restore_trash_action_button.widget.dart @@ -12,7 +12,7 @@ class RestoreTrashActionButton extends ConsumerWidget { const RestoreTrashActionButton({super.key, required this.source}); - void _onTap(BuildContext context, WidgetRef ref) async { + Future _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { return; } diff --git a/mobile/lib/presentation/widgets/action_buttons/set_album_cover.widget.dart b/mobile/lib/presentation/widgets/action_buttons/set_album_cover.widget.dart index 1d704aafe8..d080efc5b2 100644 --- a/mobile/lib/presentation/widgets/action_buttons/set_album_cover.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/set_album_cover.widget.dart @@ -22,7 +22,7 @@ class SetAlbumCoverActionButton extends ConsumerWidget { this.menuItem = false, }); - void _onTap(BuildContext context, WidgetRef ref) async { + Future _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { return; } 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 eef87f299d..ef520ea941 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 @@ -93,13 +93,13 @@ class ShareActionButton extends ConsumerWidget { return switch (source) { ActionSource.timeline => ref.read(multiSelectProvider).selectedAssets, ActionSource.viewer => switch (ref.read(assetViewerProvider).currentAsset) { - BaseAsset asset => {asset}, + final BaseAsset asset => {asset}, null => const {}, }, }; } - void _onTap(BuildContext context, WidgetRef ref) async { + Future _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { return; } @@ -108,7 +108,7 @@ class ShareActionButton extends ConsumerWidget { await _share(context, ref, fileType); } - void _onLongPress(BuildContext context, WidgetRef ref) async { + Future _onLongPress(BuildContext context, WidgetRef ref) async { if (!context.mounted) { return; } diff --git a/mobile/lib/presentation/widgets/action_buttons/share_link_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/share_link_action_button.widget.dart index b8dc69f515..dfe8fad025 100644 --- a/mobile/lib/presentation/widgets/action_buttons/share_link_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/share_link_action_button.widget.dart @@ -12,7 +12,7 @@ class ShareLinkActionButton extends ConsumerWidget { const ShareLinkActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - _onTap(BuildContext context, WidgetRef ref) async { + Future _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { return; } diff --git a/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart index 42dcfa683a..02da265f31 100644 --- a/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart @@ -18,7 +18,7 @@ class SimilarPhotosActionButton extends ConsumerWidget { const SimilarPhotosActionButton({super.key, required this.assetId, this.iconOnly = false, this.menuItem = false}); - void _onTap(BuildContext context, WidgetRef ref) async { + Future _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { return; } diff --git a/mobile/lib/presentation/widgets/action_buttons/stack_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/stack_action_button.widget.dart index 22fccf5473..b87d288a3e 100644 --- a/mobile/lib/presentation/widgets/action_buttons/stack_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/stack_action_button.widget.dart @@ -14,7 +14,7 @@ class StackActionButton extends ConsumerWidget { const StackActionButton({super.key, required this.source}); - void _onTap(BuildContext context, WidgetRef ref) async { + Future _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { return; } diff --git a/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart index e95569af45..a320d3b1b1 100644 --- a/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart @@ -20,7 +20,7 @@ class TrashActionButton extends ConsumerWidget { const TrashActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - void _onTap(BuildContext context, WidgetRef ref) async { + Future _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { return; } diff --git a/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart index 57221303a8..78984f9ef1 100644 --- a/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart @@ -4,13 +4,13 @@ import 'package:flutter/material.dart'; 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/events.model.dart'; +import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/domain/models/events.model.dart'; -import 'package:immich_mobile/domain/utils/event_stream.dart'; // used to allow performing unarchive action from different sources (without duplicating code) Future performUnArchiveAction(BuildContext context, WidgetRef ref, {required ActionSource source}) async { diff --git a/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart index 5e88735d9c..94d6588074 100644 --- a/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart @@ -17,7 +17,7 @@ class UnFavoriteActionButton extends ConsumerWidget { const UnFavoriteActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - void _onTap(BuildContext context, WidgetRef ref) async { + Future _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { return; } diff --git a/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart index e7badf129f..c9a5102a9b 100644 --- a/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart @@ -15,7 +15,7 @@ class UnStackActionButton extends ConsumerWidget { const UnStackActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - void _onTap(BuildContext context, WidgetRef ref) async { + Future _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { return; } diff --git a/mobile/lib/presentation/widgets/action_buttons/upload_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/upload_action_button.widget.dart index 599e11d467..1d09ad23a8 100644 --- a/mobile/lib/presentation/widgets/action_buttons/upload_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/upload_action_button.widget.dart @@ -24,7 +24,7 @@ class UploadActionButton extends ConsumerWidget { const UploadActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - void _onTap(BuildContext context, WidgetRef ref) async { + Future _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { return; } diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index 44abfba47e..285c6290a9 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -774,7 +774,7 @@ class AddToAlbumHeader extends ConsumerWidget { TextButton.icon( style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), // remove internal padding - minimumSize: const Size(0, 0), // allow shrinking + minimumSize: Size.zero, // allow shrinking tapTargetSize: MaterialTapTargetSize.shrinkWrap, // remove extra height ), onPressed: onCreateAlbum, @@ -797,7 +797,7 @@ class CreateAlbumButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { Future onCreateAlbum() async { - var albumName = await showDialog(context: context, builder: (context) => const NewAlbumNameModal()); + final albumName = await showDialog(context: context, builder: (context) => const NewAlbumNameModal()); if (albumName == null) { return; } @@ -839,7 +839,7 @@ class CreateAlbumButton extends ConsumerWidget { TextButton.icon( style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - minimumSize: const Size(0, 0), + minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), onPressed: onCreateAlbum, diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/appears_in_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/appears_in_details.widget.dart index 6a565fa2cd..dffda47b84 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/appears_in_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/appears_in_details.widget.dart @@ -1,4 +1,5 @@ import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -8,8 +9,8 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/album/album_tile.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart index 8c144a83bd..379f0975b1 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart @@ -53,7 +53,7 @@ class _LocationDetailsState extends ConsumerState { } } - void editLocation() async { + Future editLocation() async { await ref.read(actionProvider.notifier).editLocation(ActionSource.viewer, context); } diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/rating_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/rating_details.widget.dart index e501c2ee3e..352838c761 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/rating_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/rating_details.widget.dart @@ -41,7 +41,7 @@ class RatingDetails extends ConsumerWidget { unfilledColor: context.themeData.colorScheme.onSurface.withAlpha(100), itemSize: 40, onRatingUpdate: (rating) async { - await ref.read(actionProvider.notifier).updateRating(ActionSource.viewer, rating.round()); + await ref.read(actionProvider.notifier).updateRating(ActionSource.viewer, rating); }, onClearRating: () async { await ref.read(actionProvider.notifier).updateRating(ActionSource.viewer, null); diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/technical_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/technical_details.widget.dart index 33e0fa38f5..e97fa4889b 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/technical_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/technical_details.widget.dart @@ -99,14 +99,14 @@ class TechnicalDetails extends ConsumerWidget { static String _getFileInfo(BaseAsset asset, ExifInfo? exifInfo) { final height = asset.height; final width = asset.width; - final resolution = (width != null && height != null) ? "${width.toInt()} x ${height.toInt()}" : null; + final resolution = (width != null && height != null) ? "$width x $height" : null; final fileSize = exifInfo?.fileSize != null ? formatBytes(exifInfo!.fileSize!) : null; return switch ((fileSize, resolution)) { (null, null) => '', - (String fileSize, null) => fileSize, - (null, String resolution) => resolution, - (String fileSize, String resolution) => '$fileSize$_kSeparator$resolution', + (final String fileSize, null) => fileSize, + (null, final String resolution) => resolution, + (final String fileSize, final String resolution) => '$fileSize$_kSeparator$resolution', }; } @@ -116,9 +116,9 @@ class TechnicalDetails extends ConsumerWidget { } return switch ((exifInfo.make, exifInfo.model)) { (null, null) => null, - (String make, null) => make, - (null, String model) => model, - (String make, String model) => '$make $model', + (final String make, null) => make, + (null, final String model) => model, + (final String make, final String model) => '$make $model', }; } diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart index f5d75a6a86..23c473e618 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; class AssetStackRow extends ConsumerWidget { @@ -23,7 +23,8 @@ class AssetStackRow extends ConsumerWidget { } final showingControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); - double opacity = ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0); + final double opacity = + ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0); return IgnorePointer( ignoring: opacity < 1.0, @@ -75,7 +76,7 @@ class _StackItemState extends ConsumerState<_StackItem> { Icons.play_circle_outline_rounded, color: Colors.white, size: 16, - shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))], + shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset.zero)], ), ); const selectedDecoration = BoxDecoration( diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart index 1065ebe25e..3952dafdb2 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -167,7 +167,7 @@ class _AssetViewerState extends ConsumerState { _handleCasting(); } - void _onAssetChanged(int index) async { + Future _onAssetChanged(int index) async { _currentPage = index; final asset = await ref.read(timelineServiceProvider).getAssetAsync(index); @@ -222,7 +222,7 @@ class _AssetViewerState extends ConsumerState { _onTimelineReloadEvent(); case ViewerReloadAssetEvent(): _onViewerReloadEvent(); - case ViewerStackAssetDeletedEvent event: + case final ViewerStackAssetDeletedEvent event: _onViewerStackAssetDeletedEvent(event); default: } diff --git a/mobile/lib/presentation/widgets/asset_viewer/rating_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/rating_bar.widget.dart index b956ef103c..0ed37869be 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/rating_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/rating_bar.widget.dart @@ -64,8 +64,8 @@ class _RatingBarState extends State { } else if (dx >= totalWidth) { newRating = widget.itemCount.toDouble(); } else { - double starWithPadding = widget.itemSize + widget.starPadding; - int tappedIndex = (dx / starWithPadding).floor().clamp(0, widget.itemCount - 1); + final double starWithPadding = widget.itemSize + widget.starPadding; + final int tappedIndex = (dx / starWithPadding).floor().clamp(0, widget.itemCount - 1); newRating = tappedIndex + 1.0; if (isTap && newRating == _currentRating && _currentRating != 0) { @@ -88,7 +88,7 @@ class _RatingBarState extends State { @override Widget build(BuildContext context) { final isRTL = Directionality.of(context) == TextDirection.rtl; - final double visualAlignmentOffset = 5.0; + const double visualAlignmentOffset = 5.0; return Column( mainAxisSize: MainAxisSize.min, @@ -107,8 +107,8 @@ class _RatingBarState extends State { if (i.isOdd) { return SizedBox(width: widget.starPadding); } - int index = i ~/ 2; - bool filled = _currentRating > index; + final int index = i ~/ 2; + final bool filled = _currentRating > index; return widget.itemBuilder ?? Icon( Icons.star_rounded, diff --git a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart index ccbaf7660a..d007883ec9 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -83,7 +83,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg } @override - void didChangeAppLifecycleState(AppLifecycleState state) async { + Future didChangeAppLifecycleState(AppLifecycleState state) async { switch (state) { case AppLifecycleState.resumed: if (_shouldPlayOnForeground) { @@ -198,7 +198,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg return localAsset; } - void _onPlaybackReady() async { + Future _onPlaybackReady() async { if (!mounted || !widget.isCurrent) { return; } @@ -257,7 +257,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg _controller?.onPlaybackEnded.removeListener(_onPlaybackEnded); } - void _loadVideo() async { + Future _loadVideo() async { final nc = _controller; if (nc == null || nc.videoSource != null || !mounted) { return; diff --git a/mobile/lib/presentation/widgets/asset_viewer/viewer_bottom_app_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/viewer_bottom_app_bar.widget.dart index 1c0b600843..0e7b5661de 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/viewer_bottom_app_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/viewer_bottom_app_bar.widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_bar.widget.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; class ViewerBottomAppBar extends ConsumerWidget { const ViewerBottomAppBar({super.key}); @@ -9,7 +9,8 @@ class ViewerBottomAppBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final showingControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); - double opacity = ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0); + final double opacity = + ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0); return IgnorePointer( ignoring: opacity < 1.0, diff --git a/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart index 5997e15bf0..878a1c9405 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart @@ -40,7 +40,8 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { } final showingControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); - double opacity = ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0); + final double opacity = + ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0); final originalTheme = context.themeData; final assetForAction = [asset]; diff --git a/mobile/lib/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart index 0d0e0259fd..d5ed3f6c96 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart @@ -74,7 +74,7 @@ class _BaseDraggableScrollableSheetState extends ConsumerState color: widget.backgroundColor ?? context.colorScheme.surfaceContainer, elevation: 3.0, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(18))), - margin: const EdgeInsets.symmetric(horizontal: 0), + margin: EdgeInsets.zero, child: Column( children: [ Expanded( diff --git a/mobile/lib/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart index 945bdc9584..06d1ddc99d 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart @@ -21,10 +21,10 @@ class MapBottomSheet extends StatelessWidget { maxChildSize: 0.75, shouldCloseOnMinExtent: false, resizeOnScroll: false, - actions: [], + actions: const [], backgroundColor: context.themeData.colorScheme.surface, - slivers: [ - const SliverFillRemaining(hasScrollBody: false, child: SizedBox(height: 0, child: _ScopedMapTimeline())), + slivers: const [ + SliverFillRemaining(hasScrollBody: false, child: SizedBox(height: 0, child: _ScopedMapTimeline())), ], ); } diff --git a/mobile/lib/presentation/widgets/feature_message/feature_message_dialog.widget.dart b/mobile/lib/presentation/widgets/feature_message/feature_message_dialog.widget.dart index 9727ec29e4..2c89c68d99 100644 --- a/mobile/lib/presentation/widgets/feature_message/feature_message_dialog.widget.dart +++ b/mobile/lib/presentation/widgets/feature_message/feature_message_dialog.widget.dart @@ -4,10 +4,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/domain/models/feature_message.model.dart'; -import 'package:immich_mobile/presentation/widgets/feature_message/feature_message_placeholder.widget.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/widgets/feature_message/feature_message_placeholder.widget.dart'; Future showFeatureMessageDialog(BuildContext context) { return showGeneralDialog( diff --git a/mobile/lib/presentation/widgets/images/full_image.widget.dart b/mobile/lib/presentation/widgets/images/full_image.widget.dart index 77ea996b89..78fc0a6a21 100644 --- a/mobile/lib/presentation/widgets/images/full_image.widget.dart +++ b/mobile/lib/presentation/widgets/images/full_image.widget.dart @@ -22,7 +22,7 @@ class FullImage extends StatelessWidget { Widget build(BuildContext context) { final provider = getFullImageProvider(asset, size: size); return OctoImage( - fadeInDuration: const Duration(milliseconds: 0), + fadeInDuration: Duration.zero, fadeOutDuration: const Duration(milliseconds: 100), placeholderBuilder: placeholder != null ? (_) => placeholder! : null, image: provider, diff --git a/mobile/lib/presentation/widgets/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart index b3c58314db..9cc386e302 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -25,7 +25,7 @@ mixin CancellableImageProviderMixin on CancellableImageProvide ImageInfo? getInitialImage(CancellableImageProvider provider) { final completer = CancelableCompleter(onCancel: provider.cancel); - final cachedStream = provider.resolve(const ImageConfiguration()); + final cachedStream = provider.resolve(ImageConfiguration.empty); ImageInfo? cachedImage; final listener = ImageStreamListener((image, synchronousCall) { if (synchronousCall) { diff --git a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart index c42d365464..7d71f0296d 100644 --- a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart +++ b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart @@ -285,7 +285,7 @@ class _TileOverlayIcon extends StatelessWidget { icon, color: Colors.white, size: 16, - shadows: [const Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))], + shadows: const [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset.zero)], ); } } diff --git a/mobile/lib/presentation/widgets/map/map_utils.dart b/mobile/lib/presentation/widgets/map/map_utils.dart index 80df5995b6..3ce7b2e055 100644 --- a/mobile/lib/presentation/widgets/map/map_utils.dart +++ b/mobile/lib/presentation/widgets/map/map_utils.dart @@ -71,7 +71,7 @@ class MapUtils { bool silent = false, }) async { try { - bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); + final bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled && !silent) { unawaited(showDialog(context: context, builder: (context) => _LocationServiceDisabledDialog(context))); return (null, LocationPermission.deniedForever); @@ -98,7 +98,7 @@ class MapUtils { return (null, LocationPermission.deniedForever); } - Position currentUserLocation = await Geolocator.getCurrentPosition( + final Position currentUserLocation = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.high, distanceFilter: 0, diff --git a/mobile/lib/presentation/widgets/memory/memory_card.widget.dart b/mobile/lib/presentation/widgets/memory/memory_card.widget.dart index 2f7a616632..2a88de8e0a 100644 --- a/mobile/lib/presentation/widgets/memory/memory_card.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_card.widget.dart @@ -55,7 +55,7 @@ class DriftMemoryCard extends StatelessWidget { } if (asset.isImage) { - return FullImage(asset, fit: fit, size: const Size(double.infinity, double.infinity)); + return FullImage(asset, fit: fit, size: Size.infinite); } return Center( diff --git a/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart b/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart index c194bbc684..6e66ff47ce 100644 --- a/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart @@ -6,9 +6,9 @@ import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; +import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:scroll_date_picker/scroll_date_picker.dart'; -import 'package:immich_mobile/utils/debug_print.dart'; class DriftPersonBirthdayEditForm extends ConsumerStatefulWidget { final DriftPerson person; @@ -28,7 +28,7 @@ class _DriftPersonNameEditFormState extends ConsumerState saveBirthday() async { try { final result = await ref.read(driftPeopleServiceProvider).updateBrithday(widget.person.id, _selectedDate); diff --git a/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart b/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart index 6de19000e0..2eaac2ebf5 100644 --- a/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart @@ -6,8 +6,8 @@ import 'package:immich_mobile/domain/models/person.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/infrastructure/people.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/utils/debug_print.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; class DriftPersonNameEditForm extends ConsumerStatefulWidget { final DriftPerson person; @@ -27,7 +27,7 @@ class _DriftPersonNameEditFormState extends ConsumerState onEdit(String personId, String newName) async { try { final result = await ref.read(driftPeopleServiceProvider).updateName(personId, newName); if (result != 0) { diff --git a/mobile/lib/presentation/widgets/people/person_option_sheet.widget.dart b/mobile/lib/presentation/widgets/people/person_option_sheet.widget.dart index b374d48417..b0ded02624 100644 --- a/mobile/lib/presentation/widgets/people/person_option_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_option_sheet.widget.dart @@ -11,7 +11,7 @@ class PersonOptionSheet extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - TextStyle textStyle = Theme.of(context).textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w600); + final TextStyle textStyle = Theme.of(context).textTheme.bodyLarge!.copyWith(fontWeight: FontWeight.w600); return SafeArea( child: Padding( diff --git a/mobile/lib/presentation/widgets/timeline/header.widget.dart b/mobile/lib/presentation/widgets/timeline/header.widget.dart index 3eff305251..d73d024efb 100644 --- a/mobile/lib/presentation/widgets/timeline/header.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/header.widget.dart @@ -6,8 +6,8 @@ import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; class TimelineHeader extends HookConsumerWidget { diff --git a/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart b/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart index 27f523a2a8..f5e3493a8e 100644 --- a/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart @@ -590,7 +590,7 @@ class _SlideFadeTransition extends StatelessWidget { animation: _animation, builder: (context, child) => _animation.value == 0.0 ? const SizedBox() : child!, child: SlideTransition( - position: Tween(begin: const Offset(0.3, 0.0), end: const Offset(0.0, 0.0)).animate(_animation), + position: Tween(begin: const Offset(0.3, 0.0), end: Offset.zero).animate(_animation), child: FadeTransition(opacity: _animation, child: _child), ), ); diff --git a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart index 20a4b4257e..5bd39deb8a 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart @@ -199,7 +199,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBi switch (event) { case ScrollToTopEvent(): _scrollToTop(); - case ScrollToDateEvent scrollToDateEvent: + case final ScrollToDateEvent scrollToDateEvent: _scrollToDate(scrollToDateEvent.date); case TimelineReloadEvent(): setState(() {}); diff --git a/mobile/lib/providers/album/album_title.provider.dart b/mobile/lib/providers/album/album_title.provider.dart index bf812a01d8..b38c2929fb 100644 --- a/mobile/lib/providers/album/album_title.provider.dart +++ b/mobile/lib/providers/album/album_title.provider.dart @@ -3,11 +3,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; class AlbumTitleNotifier extends StateNotifier { AlbumTitleNotifier() : super(""); - setAlbumTitle(String title) { + void setAlbumTitle(String title) { state = title; } - clearAlbumTitle() { + void clearAlbumTitle() { state = ""; } } diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index ad0940b776..2b52973c0a 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -35,7 +35,7 @@ class AppLifeCycleNotifier extends StateNotifier { return state; } - void handleAppResume() async { + Future handleAppResume() async { state = AppLifeCycleEnum.resumed; // Prevent overlapping resume operations diff --git a/mobile/lib/providers/asset_viewer/download.provider.dart b/mobile/lib/providers/asset_viewer/download.provider.dart index 37d3392d88..2c4854bdb0 100644 --- a/mobile/lib/providers/asset_viewer/download.provider.dart +++ b/mobile/lib/providers/asset_viewer/download.provider.dart @@ -37,7 +37,7 @@ class DownloadStateNotifier extends StateNotifier { ); } - void cancelDownload(String id) async { + Future cancelDownload(String id) async { final isCanceled = await _downloadService.cancelDownload(id); if (isCanceled) { @@ -55,5 +55,5 @@ class DownloadStateNotifier extends StateNotifier { } final downloadStateProvider = StateNotifierProvider( - ((ref) => DownloadStateNotifier(ref.watch(downloadServiceProvider))), + (ref) => DownloadStateNotifier(ref.watch(downloadServiceProvider)), ); diff --git a/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart b/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart index 8bd0581061..51119f4ba2 100644 --- a/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart +++ b/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart @@ -9,11 +9,11 @@ import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; final shareIntentUploadProvider = StateNotifierProvider>( - ((ref) => ShareIntentUploadStateNotifier( + (ref) => ShareIntentUploadStateNotifier( ref.watch(appRouterProvider), ref.read(foregroundUploadServiceProvider), ref.read(shareIntentServiceProvider), - )), + ), ); class ShareIntentUploadStateNotifier extends StateNotifier> { diff --git a/mobile/lib/providers/auth.provider.dart b/mobile/lib/providers/auth.provider.dart index 23ccea8025..2ed4cbcf00 100644 --- a/mobile/lib/providers/auth.provider.dart +++ b/mobile/lib/providers/auth.provider.dart @@ -134,7 +134,7 @@ class AuthNotifier extends StateNotifier { await _widgetService.writeCredentials(serverEndpoint, accessToken, customHeaders); // Get the deviceid from the store if it exists, otherwise generate a new one - String deviceId = Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid; + final String deviceId = Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid; UserDto? user = _userService.tryGetMyUser(); diff --git a/mobile/lib/providers/backup/drift_backup.provider.dart b/mobile/lib/providers/backup/drift_backup.provider.dart index bf2b7cae4a..b43fec023c 100644 --- a/mobile/lib/providers/backup/drift_backup.provider.dart +++ b/mobile/lib/providers/backup/drift_backup.provider.dart @@ -2,16 +2,15 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:logging/logging.dart'; - import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/utils/upload_speed_calculator.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/services/foreground_upload.service.dart'; import 'package:immich_mobile/services/background_upload.service.dart'; +import 'package:immich_mobile/services/foreground_upload.service.dart'; +import 'package:immich_mobile/utils/upload_speed_calculator.dart'; +import 'package:logging/logging.dart'; class EnqueueStatus { final int enqueueCount; diff --git a/mobile/lib/providers/cast.provider.dart b/mobile/lib/providers/cast.provider.dart index b298514d67..943ac930ec 100644 --- a/mobile/lib/providers/cast.provider.dart +++ b/mobile/lib/providers/cast.provider.dart @@ -58,7 +58,6 @@ class CastNotifier extends StateNotifier { switch (type) { case CastDestinationType.googleCast: await _gCastService.connect(device); - break; } } diff --git a/mobile/lib/providers/gallery_permission.provider.dart b/mobile/lib/providers/gallery_permission.provider.dart index 6e4fc69926..315c67a214 100644 --- a/mobile/lib/providers/gallery_permission.provider.dart +++ b/mobile/lib/providers/gallery_permission.provider.dart @@ -12,7 +12,7 @@ class GalleryPermissionNotifier extends StateNotifier { getGalleryPermissionStatus(); } - get hasPermission => state.isGranted || state.isLimited; + bool get hasPermission => state.isGranted || state.isLimited; /// Requests the gallery permission Future requestGalleryPermission() async { diff --git a/mobile/lib/providers/haptic_feedback.provider.dart b/mobile/lib/providers/haptic_feedback.provider.dart index 711c6fa4e2..e1ce5c8d0d 100644 --- a/mobile/lib/providers/haptic_feedback.provider.dart +++ b/mobile/lib/providers/haptic_feedback.provider.dart @@ -13,31 +13,31 @@ class HapticNotifier extends StateNotifier { HapticNotifier(this._ref) : super(null); - selectionClick() { + void selectionClick() { if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { HapticFeedback.selectionClick(); } } - lightImpact() { + void lightImpact() { if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { HapticFeedback.lightImpact(); } } - mediumImpact() { + void mediumImpact() { if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { HapticFeedback.mediumImpact(); } } - heavyImpact() { + void heavyImpact() { if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { HapticFeedback.heavyImpact(); } } - vibrate() { + void vibrate() { if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { HapticFeedback.vibrate(); } diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index 52e2d9e0b9..7e08686078 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -110,7 +110,7 @@ class ActionNotifier extends Notifier { return switch (source) { ActionSource.timeline => ref.read(multiSelectProvider).selectedAssets, ActionSource.viewer => switch (ref.read(assetViewerProvider).currentAsset) { - BaseAsset asset => {asset}, + final BaseAsset asset => {asset}, null => const {}, }, }; @@ -273,7 +273,7 @@ class ActionNotifier extends Notifier { Future deleteLocal(ActionSource source, BuildContext context) async { final assets = _getAssets(source); - bool? backedUpOnly = assets.every((asset) => asset.storage == AssetState.merged) + final bool? backedUpOnly = assets.every((asset) => asset.storage == AssetState.merged) ? true : await showDialog( context: context, diff --git a/mobile/lib/providers/infrastructure/timeline.provider.dart b/mobile/lib/providers/infrastructure/timeline.provider.dart index a67b8dd822..e5ac1389e9 100644 --- a/mobile/lib/providers/infrastructure/timeline.provider.dart +++ b/mobile/lib/providers/infrastructure/timeline.provider.dart @@ -24,7 +24,7 @@ final timelineServiceProvider = Provider( }, // Empty dependencies to inform the framework that this provider // might be used in a ProviderScope - dependencies: [], + dependencies: const [], ); final timelineFactoryProvider = Provider( diff --git a/mobile/lib/providers/local_auth.provider.dart b/mobile/lib/providers/local_auth.provider.dart index 44fc5ad80c..d2860975bb 100644 --- a/mobile/lib/providers/local_auth.provider.dart +++ b/mobile/lib/providers/local_auth.provider.dart @@ -48,15 +48,12 @@ class LocalAuthNotifier extends StateNotifier { case "NotEnrolled": _log.warning("User is not enrolled in biometrics"); errorMessage = "biometric_no_options".tr(); - break; case "NotAvailable": _log.warning("Biometric authentication is not available"); errorMessage = "biometric_not_available".tr(); - break; case "LockedOut": _log.warning("User is locked out of biometric authentication"); errorMessage = "biometric_locked_out".tr(); - break; default: _log.warning("Failed to authenticate with unknown reason"); errorMessage = 'failed_to_authenticate'.tr(); diff --git a/mobile/lib/providers/map/map_marker.provider.dart b/mobile/lib/providers/map/map_marker.provider.dart index 38432eab6b..ab73f94c10 100644 --- a/mobile/lib/providers/map/map_marker.provider.dart +++ b/mobile/lib/providers/map/map_marker.provider.dart @@ -8,8 +8,8 @@ final mapMarkersProvider = FutureProvider.autoDispose>((ref) asy final mapState = ref.read(mapStateNotifierProvider); DateTime? fileCreatedAfter; bool? isFavorite; - bool isIncludeArchived = mapState.includeArchived; - bool isWithPartners = mapState.withPartners; + final bool isIncludeArchived = mapState.includeArchived; + final bool isWithPartners = mapState.withPartners; if (mapState.relativeTime != 0) { fileCreatedAfter = DateTime.now().subtract(Duration(days: mapState.relativeTime)); diff --git a/mobile/lib/providers/oauth.provider.dart b/mobile/lib/providers/oauth.provider.dart index 14b3353943..fd39e0907d 100644 --- a/mobile/lib/providers/oauth.provider.dart +++ b/mobile/lib/providers/oauth.provider.dart @@ -1,5 +1,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/services/oauth.service.dart'; import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/services/oauth.service.dart'; final oAuthServiceProvider = Provider((ref) => OAuthService(ref.watch(apiServiceProvider))); diff --git a/mobile/lib/providers/server_info.provider.dart b/mobile/lib/providers/server_info.provider.dart index 98300894f9..bf83b36f54 100644 --- a/mobile/lib/providers/server_info.provider.dart +++ b/mobile/lib/providers/server_info.provider.dart @@ -56,11 +56,11 @@ class ServerInfoNotifier extends StateNotifier { } } - _checkServerVersionMismatch(ServerVersion serverVersion, {ServerVersion? latestVersion}) async { + Future _checkServerVersionMismatch(ServerVersion serverVersion, {ServerVersion? latestVersion}) async { state = state.copyWith(serverVersion: serverVersion, latestVersion: latestVersion); - var packageInfo = await PackageInfo.fromPlatform(); - SemVer clientVersion = SemVer.fromString(packageInfo.version); + final packageInfo = await PackageInfo.fromPlatform(); + final SemVer clientVersion = SemVer.fromString(packageInfo.version); if (serverVersion < clientVersion || (latestVersion != null && serverVersion < latestVersion)) { state = state.copyWith(versionStatus: VersionStatus.serverOutOfDate); @@ -75,12 +75,12 @@ class ServerInfoNotifier extends StateNotifier { state = state.copyWith(versionStatus: VersionStatus.upToDate); } - handleReleaseInfo(ServerVersion serverVersion, ServerVersion? latestVersion) { + void handleReleaseInfo(ServerVersion serverVersion, ServerVersion? latestVersion) { // Update local server version _checkServerVersionMismatch(serverVersion, latestVersion: latestVersion); } - getServerFeatures() async { + Future getServerFeatures() async { final serverFeatures = await _serverInfoService.getServerFeatures(); if (serverFeatures == null) { return; @@ -88,7 +88,7 @@ class ServerInfoNotifier extends StateNotifier { state = state.copyWith(serverFeatures: serverFeatures); } - getServerConfig() async { + Future getServerConfig() async { final serverConfig = await _serverInfoService.getServerConfig(); if (serverConfig == null) { return; diff --git a/mobile/lib/providers/sync_status.provider.dart b/mobile/lib/providers/sync_status.provider.dart index 8d7266abf7..b7d4f8bdc3 100644 --- a/mobile/lib/providers/sync_status.provider.dart +++ b/mobile/lib/providers/sync_status.provider.dart @@ -7,7 +7,7 @@ enum SyncStatus { success, error; - localized() { + String localized() { return switch (this) { SyncStatus.idle => "idle".tr(), SyncStatus.syncing => "running".tr(), diff --git a/mobile/lib/providers/timeline/multiselect.provider.dart b/mobile/lib/providers/timeline/multiselect.provider.dart index 10c8bb86b6..cb053e0041 100644 --- a/mobile/lib/providers/timeline/multiselect.provider.dart +++ b/mobile/lib/providers/timeline/multiselect.provider.dart @@ -102,7 +102,7 @@ class MultiSelectNotifier extends Notifier { } /// Bucket bulk operations - void selectBucket(int offset, int bucketCount) async { + Future selectBucket(int offset, int bucketCount) async { final assets = await _timelineService.loadAssets(offset, bucketCount); final selectedAssets = state.selectedAssets.toSet(); @@ -111,7 +111,7 @@ class MultiSelectNotifier extends Notifier { state = state.copyWith(selectedAssets: selectedAssets); } - void deselectBucket(int offset, int bucketCount) async { + Future deselectBucket(int offset, int bucketCount) async { final assets = await _timelineService.loadAssets(offset, bucketCount); final selectedAssets = state.selectedAssets.toSet(); @@ -120,7 +120,7 @@ class MultiSelectNotifier extends Notifier { state = state.copyWith(selectedAssets: selectedAssets); } - void toggleBucketSelection(int offset, int bucketCount) async { + Future toggleBucketSelection(int offset, int bucketCount) async { final assets = await _timelineService.loadAssets(offset, bucketCount); toggleBucketSelectionByAssets(assets); } diff --git a/mobile/lib/providers/upload_profile_image.provider.dart b/mobile/lib/providers/upload_profile_image.provider.dart index 77772b0205..5e7f7fc732 100644 --- a/mobile/lib/providers/upload_profile_image.provider.dart +++ b/mobile/lib/providers/upload_profile_image.provider.dart @@ -66,7 +66,7 @@ class UploadProfileImageNotifier extends StateNotifier Future upload(XFile file, {String? fileName}) async { state = state.copyWith(status: UploadProfileStatus.loading); - var profileImagePath = await _userService.createProfileImage(fileName ?? file.name, await file.readAsBytes()); + final profileImagePath = await _userService.createProfileImage(fileName ?? file.name, await file.readAsBytes()); if (profileImagePath != null) { dPrint(() => "Successfully upload profile image"); @@ -80,5 +80,5 @@ class UploadProfileImageNotifier extends StateNotifier } final uploadProfileImageProvider = StateNotifierProvider( - ((ref) => UploadProfileImageNotifier(ref.watch(userServiceProvider))), + (ref) => UploadProfileImageNotifier(ref.watch(userServiceProvider)), ); diff --git a/mobile/lib/providers/user.provider.dart b/mobile/lib/providers/user.provider.dart index 5a56b65793..622847b0c2 100644 --- a/mobile/lib/providers/user.provider.dart +++ b/mobile/lib/providers/user.provider.dart @@ -14,7 +14,7 @@ class CurrentUserProvider extends StateNotifier { final UserService _userService; late final StreamSubscription streamSub; - refresh() async { + Future refresh() async { try { await _userService.refreshMyUser(); } catch (_) {} diff --git a/mobile/lib/providers/websocket.provider.dart b/mobile/lib/providers/websocket.provider.dart index fa05f9a4cd..a7c08457af 100644 --- a/mobile/lib/providers/websocket.provider.dart +++ b/mobile/lib/providers/websocket.provider.dart @@ -71,7 +71,7 @@ class WebsocketNotifier extends StateNotifier { final endpoint = Uri.parse(Store.get(StoreKey.serverEndpoint)); dPrint(() => "Attempting to connect to websocket"); // Configure socket transports must be specified - Socket socket = io( + final Socket socket = io( endpoint.origin, OptionBuilder() .setPath("${endpoint.path}/socket.io") @@ -107,7 +107,7 @@ class WebsocketNotifier extends StateNotifier { socket.on('on_config_update', _handleOnConfigUpdate); socket.on('on_new_release', _handleReleaseUpdates); } catch (e) { - dPrint(() => "[WEBSOCKET] Catch Websocket Error - ${e.toString()}"); + dPrint(() => "[WEBSOCKET] Catch Websocket Error - $e"); } } } @@ -147,7 +147,7 @@ class WebsocketNotifier extends StateNotifier { _ref.read(serverInfoProvider.notifier).getServerConfig(); } - _handleReleaseUpdates(dynamic data) { + void _handleReleaseUpdates(dynamic data) { // Json guard if (data is! Map) { return; diff --git a/mobile/lib/repositories/asset_media.repository.dart b/mobile/lib/repositories/asset_media.repository.dart index 4c9b6f6009..5bfb18a00f 100644 --- a/mobile/lib/repositories/asset_media.repository.dart +++ b/mobile/lib/repositories/asset_media.repository.dart @@ -37,9 +37,9 @@ class AssetMediaRepository { Future _androidSupportsTrash() async { if (Platform.isAndroid) { - DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); - AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; - int sdkVersion = androidInfo.version.sdkInt; + final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + final AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + final int sdkVersion = androidInfo.version.sdkInt; return sdkVersion >= 31; } return false; diff --git a/mobile/lib/repositories/auth_api.repository.dart b/mobile/lib/repositories/auth_api.repository.dart index 05dc7f103a..447826b9aa 100644 --- a/mobile/lib/repositories/auth_api.repository.dart +++ b/mobile/lib/repositories/auth_api.repository.dart @@ -32,7 +32,7 @@ class AuthApiRepository extends ApiRepository { await _apiService.authenticationApi.logout().timeout(const Duration(seconds: 7)); } - _mapLoginReponse(LoginResponseDto dto) { + LoginResponse _mapLoginReponse(LoginResponseDto dto) { return LoginResponse( accessToken: dto.accessToken, isAdmin: dto.isAdmin, diff --git a/mobile/lib/repositories/drift_album_api_repository.dart b/mobile/lib/repositories/drift_album_api_repository.dart index 3a654b7511..e0d4cc4632 100644 --- a/mobile/lib/repositories/drift_album_api_repository.dart +++ b/mobile/lib/repositories/drift_album_api_repository.dart @@ -36,7 +36,8 @@ class DriftAlbumApiRepository extends ApiRepository { Future<({List removed, List failed})> removeAssets(String albumId, Iterable assetIds) async { final response = await checkNull(_api.removeAssetFromAlbum(albumId, BulkIdsDto(ids: assetIds.toList()))); - final List removed = [], failed = []; + final List removed = []; + final List failed = []; for (final dto in response) { if (dto.success) { removed.add(dto.id); @@ -55,7 +56,8 @@ class DriftAlbumApiRepository extends ApiRepository { final response = await checkNull( _api.addAssetsToAlbum(albumId, BulkIdsDto(ids: assetIds.toList()), abortTrigger: abortTrigger), ); - final List added = [], failed = []; + final List added = []; + final List failed = []; for (final dto in response) { if (dto.success) { added.add(dto.id); diff --git a/mobile/lib/repositories/gcast.repository.dart b/mobile/lib/repositories/gcast.repository.dart index db3e0f45d0..b8ffe79b04 100644 --- a/mobile/lib/repositories/gcast.repository.dart +++ b/mobile/lib/repositories/gcast.repository.dart @@ -1,7 +1,7 @@ import 'package:cast/device.dart'; +import 'package:cast/discovery_service.dart'; import 'package:cast/session.dart'; import 'package:cast/session_manager.dart'; -import 'package:cast/discovery_service.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; final gCastRepositoryProvider = Provider((_) { diff --git a/mobile/lib/repositories/upload.repository.dart b/mobile/lib/repositories/upload.repository.dart index 68522490d8..1ebc9825f4 100644 --- a/mobile/lib/repositories/upload.repository.dart +++ b/mobile/lib/repositories/upload.repository.dart @@ -4,13 +4,13 @@ import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:http/http.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; -import 'package:logging/logging.dart'; -import 'package:http/http.dart'; import 'package:immich_mobile/utils/debug_print.dart'; +import 'package:logging/logging.dart'; final uploadRepositoryProvider = Provider((ref) => UploadRepository()); @@ -144,7 +144,7 @@ class UploadRepository { logger.warning("Upload $logContext was cancelled"); return UploadResult.cancelled(); } catch (error, stackTrace) { - logger.warning("Error uploading $logContext: ${error.toString()}: $stackTrace"); + logger.warning("Error uploading $logContext: $error: $stackTrace"); return UploadResult.error(errorMessage: error.toString()); } } diff --git a/mobile/lib/routing/duplicate_guard.dart b/mobile/lib/routing/duplicate_guard.dart index c55c7318d0..bffe6064ee 100644 --- a/mobile/lib/routing/duplicate_guard.dart +++ b/mobile/lib/routing/duplicate_guard.dart @@ -5,7 +5,7 @@ import 'package:immich_mobile/utils/debug_print.dart'; class DuplicateGuard extends AutoRouteGuard { const DuplicateGuard(); @override - void onNavigation(NavigationResolver resolver, StackRouter router) async { + Future onNavigation(NavigationResolver resolver, StackRouter router) async { // Duplicate navigation if (resolver.route.name == router.current.name) { dPrint(() => 'DuplicateGuard: Preventing duplicate route navigation for ${resolver.route.name}'); diff --git a/mobile/lib/routing/locked_guard.dart b/mobile/lib/routing/locked_guard.dart index 38484538e0..da0a025bca 100644 --- a/mobile/lib/routing/locked_guard.dart +++ b/mobile/lib/routing/locked_guard.dart @@ -21,7 +21,7 @@ class LockedGuard extends AutoRouteGuard { LockedGuard(this._apiService, this._secureStorageService, this._localAuth); @override - void onNavigation(NavigationResolver resolver, StackRouter router) async { + Future onNavigation(NavigationResolver resolver, StackRouter router) async { final authStatus = await _apiService.authenticationApi.getAuthStatus(); if (authStatus == null) { @@ -62,13 +62,10 @@ class LockedGuard extends AutoRouteGuard { switch (error.code) { case auth_error.notAvailable: _log.severe("notAvailable: $error"); - break; case auth_error.notEnrolled: _log.severe("not enrolled"); - break; default: _log.severe("error"); - break; } resolver.next(false); diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 34d29be945..ea33e4ba25 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -38,7 +38,6 @@ import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; import 'package:immich_mobile/presentation/pages/cleanup_preview.page.dart'; import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart'; -import 'package:immich_mobile/presentation/pages/feature_message/whats_new.page.dart'; import 'package:immich_mobile/presentation/pages/download_info.page.dart'; import 'package:immich_mobile/presentation/pages/drift_activities.page.dart'; import 'package:immich_mobile/presentation/pages/drift_album.page.dart'; @@ -66,6 +65,7 @@ import 'package:immich_mobile/presentation/pages/drift_trash.page.dart'; import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart'; import 'package:immich_mobile/presentation/pages/drift_video.page.dart'; import 'package:immich_mobile/presentation/pages/edit/drift_edit.page.dart'; +import 'package:immich_mobile/presentation/pages/feature_message/whats_new.page.dart'; import 'package:immich_mobile/presentation/pages/local_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/profile/profile_picture_crop.page.dart'; import 'package:immich_mobile/presentation/pages/search/drift_search.page.dart'; diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index ab05ffc18f..f5e4c5c33e 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -53,7 +53,7 @@ class ApiService { _apiClient.client = NetworkRepository.client; } - setEndpoint(String endpoint) { + void setEndpoint(String endpoint) { _apiClient.basePath = endpoint; _apiClient.client = NetworkRepository.client; usersApi = UsersApi(_apiClient); @@ -118,7 +118,7 @@ class ApiService { } try { - await setEndpoint(serverUrl); + setEndpoint(serverUrl); await serverInfoApi.pingServer().timeout(const Duration(seconds: 5)); } on TimeoutException catch (_) { return false; @@ -155,7 +155,7 @@ class ApiService { } Future setDeviceInfoHeader() async { - DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); + final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); if (Platform.isIOS) { final iosInfo = await deviceInfoPlugin.iosInfo; diff --git a/mobile/lib/services/auth.service.dart b/mobile/lib/services/auth.service.dart index 0de22fd124..0e02c7b1b9 100644 --- a/mobile/lib/services/auth.service.dart +++ b/mobile/lib/services/auth.service.dart @@ -5,8 +5,8 @@ import 'package:immich_mobile/domain/models/settings_key.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/models/auth/login_response.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; diff --git a/mobile/lib/services/background_upload.service.dart b/mobile/lib/services/background_upload.service.dart index ec731c6f68..5b379ff890 100644 --- a/mobile/lib/services/background_upload.service.dart +++ b/mobile/lib/services/background_upload.service.dart @@ -171,7 +171,7 @@ class BackgroundUploadService { const batchSize = 100; final batch = candidates.take(batchSize).toList(); - List tasks = []; + final List tasks = []; for (final asset in batch) { final task = await getUploadTask(asset); @@ -205,7 +205,7 @@ class BackgroundUploadService { return _uploadRepository.start(); } - void _handleTaskStatusUpdate(TaskStatusUpdate update) async { + Future _handleTaskStatusUpdate(TaskStatusUpdate update) async { switch (update.status) { case TaskStatus.complete: unawaited(_handleLivePhoto(update)); @@ -219,8 +219,6 @@ class BackgroundUploadService { } } - break; - default: break; } @@ -295,7 +293,7 @@ class BackgroundUploadService { final extension = p.extension(file.path).isNotEmpty ? p.extension(file.path) : p.extension(asset.name); final originalFileName = p.setExtension(fileName, extension); - String metadata = UploadTaskMetadata( + final String metadata = UploadTaskMetadata( localAssetId: asset.id, isLivePhotos: entity.isLivePhoto, livePhotoVideoId: '', diff --git a/mobile/lib/services/download.service.dart b/mobile/lib/services/download.service.dart index b84d6ebfe8..de8e8af3f5 100644 --- a/mobile/lib/services/download.service.dart +++ b/mobile/lib/services/download.service.dart @@ -63,7 +63,7 @@ class DownloadService { onVideoDownloadStatus?.call(update); } - void _onLivePhotoRecordComplete(TaskRecord record) async { + Future _onLivePhotoRecordComplete(TaskRecord record) async { final livePhotosId = LivePhotosMetadata.fromJson(record.task.metaData).id; await _saveLivePhotos(livePhotosId); } diff --git a/mobile/lib/services/folder.service.dart b/mobile/lib/services/folder.service.dart index 543c7231d6..011acbe869 100644 --- a/mobile/lib/services/folder.service.dart +++ b/mobile/lib/services/folder.service.dart @@ -18,7 +18,7 @@ class FolderService { final paths = await _folderApiRepository.getAllUniquePaths(); // Create folder structure - Map> folderMap = {}; + final Map> folderMap = {}; for (String fullPath in paths) { if (fullPath == '/') { @@ -30,12 +30,12 @@ class FolderService { fullPath = '/$fullPath'; } - List segments = fullPath.split('/')..removeWhere((s) => s.isEmpty); + final List segments = fullPath.split('/')..removeWhere((s) => s.isEmpty); String currentPath = ''; for (int i = 0; i < segments.length; i++) { - String parentPath = currentPath.isEmpty ? '_root_' : currentPath; + final String parentPath = currentPath.isEmpty ? '_root_' : currentPath; currentPath = i == 0 ? '/${segments[i]}' : '$currentPath/${segments[i]}'; if (!folderMap.containsKey(parentPath)) { @@ -55,7 +55,7 @@ class FolderService { } void attachSubfolders(RecursiveFolder folder) { - String fullPath = folder.path.isEmpty ? '/${folder.name}' : '${folder.path}/${folder.name}'; + final String fullPath = folder.path.isEmpty ? '/${folder.name}' : '${folder.path}/${folder.name}'; if (folderMap.containsKey(fullPath)) { folder.subfolders.addAll(folderMap[fullPath]!); @@ -67,7 +67,7 @@ class FolderService { } } - List rootSubfolders = folderMap['_root_'] ?? []; + final List rootSubfolders = folderMap['_root_'] ?? []; // Sort root subfolders based on order parameter rootSubfolders.sort((a, b) => order == SortOrder.desc ? b.name.compareTo(a.name) : a.name.compareTo(b.name)); @@ -83,7 +83,7 @@ class FolderService { if (folder is RecursiveFolder) { String fullPath = folder.path.isEmpty ? folder.name : '${folder.path}/${folder.name}'; fullPath = fullPath[0] == '/' ? fullPath.substring(1) : fullPath; - var result = await _folderApiRepository.getAssetsForPath(fullPath); + final result = await _folderApiRepository.getAssetsForPath(fullPath); if (order == SortOrder.desc) { result.sort((a, b) => b.createdAt.compareTo(a.createdAt)); diff --git a/mobile/lib/services/foreground_upload.service.dart b/mobile/lib/services/foreground_upload.service.dart index cce1241473..7c0352a00e 100644 --- a/mobile/lib/services/foreground_upload.service.dart +++ b/mobile/lib/services/foreground_upload.service.dart @@ -398,7 +398,7 @@ class ForegroundUploadService { } } } catch (error, stackTrace) { - _logger.severe(() => "Error backup asset: ${error.toString()}", stackTrace); + _logger.severe(() => "Error backup asset: $error", stackTrace); callbacks.onError?.call(asset.localId!, error.toString()); } finally { if (Platform.isIOS) { @@ -406,7 +406,7 @@ class ForegroundUploadService { await file?.delete(); await livePhotoFile?.delete(); } catch (error, stackTrace) { - _logger.severe(() => "ERROR deleting file: ${error.toString()}", stackTrace); + _logger.severe(() => "ERROR deleting file: $error", stackTrace); } } } diff --git a/mobile/lib/services/gcast.service.dart b/mobile/lib/services/gcast.service.dart index dcf7685237..d9fc44a34d 100644 --- a/mobile/lib/services/gcast.service.dart +++ b/mobile/lib/services/gcast.service.dart @@ -62,7 +62,6 @@ class GCastService { switch (message['type']) { case "MEDIA_STATUS": _handleMediaStatus(message); - break; } } @@ -77,13 +76,10 @@ class GCastService { switch (status['playerState']) { case "PLAYING": onCastState?.call(CastState.playing); - break; case "PAUSED": onCastState?.call(CastState.paused); - break; case "BUFFERING": onCastState?.call(CastState.buffering); - break; case "IDLE": onCastState?.call(CastState.idle); @@ -91,8 +87,6 @@ class GCastService { if (status["idleReason"] == "FINISHED") { _mediaStatusPollingTimer?.cancel(); } - - break; } if (status["media"] != null && status["media"]["duration"] != null) { @@ -147,7 +141,7 @@ class GCastService { return bufferedExpiration.isAfter(DateTime.now()); } - void loadMedia(RemoteAsset asset, bool reload) async { + Future loadMedia(RemoteAsset asset, bool reload) async { if (!isConnected) { return; } else if (asset.id == currentAssetId && !reload) { diff --git a/mobile/lib/services/search.service.dart b/mobile/lib/services/search.service.dart index 0330c8485c..e64c529f26 100644 --- a/mobile/lib/services/search.service.dart +++ b/mobile/lib/services/search.service.dart @@ -34,7 +34,7 @@ class SearchService { model: model, ); } catch (e) { - dPrint(() => "[ERROR] [getSearchSuggestions] ${e.toString()}"); + dPrint(() => "[ERROR] [getSearchSuggestions] $e"); return []; } } diff --git a/mobile/lib/services/server_info.service.dart b/mobile/lib/services/server_info.service.dart index 460e135421..fb8b347caa 100644 --- a/mobile/lib/services/server_info.service.dart +++ b/mobile/lib/services/server_info.service.dart @@ -21,7 +21,7 @@ class ServerInfoService { return ServerDiskInfo.fromDto(dto); } } catch (e) { - dPrint(() => "Error [getDiskInfo] ${e.toString()}"); + dPrint(() => "Error [getDiskInfo] $e"); } return null; } @@ -33,7 +33,7 @@ class ServerInfoService { return ServerVersion.fromDto(dto); } } catch (e) { - dPrint(() => "Error [getServerVersion] ${e.toString()}"); + dPrint(() => "Error [getServerVersion] $e"); } return null; } @@ -45,7 +45,7 @@ class ServerInfoService { return ServerFeatures.fromDto(dto); } } catch (e) { - dPrint(() => "Error [getServerFeatures] ${e.toString()}"); + dPrint(() => "Error [getServerFeatures] $e"); } return null; } @@ -57,7 +57,7 @@ class ServerInfoService { return ServerConfig.fromDto(dto); } } catch (e) { - dPrint(() => "Error [getServerConfig] ${e.toString()}"); + dPrint(() => "Error [getServerConfig] $e"); } return null; } diff --git a/mobile/lib/theme/dynamic_theme.dart b/mobile/lib/theme/dynamic_theme.dart index 7f7c4d05d7..088679b508 100644 --- a/mobile/lib/theme/dynamic_theme.dart +++ b/mobile/lib/theme/dynamic_theme.dart @@ -1,6 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:dynamic_color/dynamic_color.dart'; - +import 'package:flutter/material.dart'; import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/utils/debug_print.dart'; diff --git a/mobile/lib/utils/bytes_units.dart b/mobile/lib/utils/bytes_units.dart index 5eb15221fe..2c5c5f18a9 100644 --- a/mobile/lib/utils/bytes_units.dart +++ b/mobile/lib/utils/bytes_units.dart @@ -22,6 +22,6 @@ String formatHumanReadableBytes(int bytes, int decimals) { return "0 B"; } const suffixes = ["B", "KiB", "MiB", "GiB", "TiB"]; - var i = (log(bytes) / log(1024)).floor(); + final i = (log(bytes) / log(1024)).floor(); return '${(bytes / pow(1024, i)).toStringAsFixed(decimals)} ${suffixes[i]}'; } diff --git a/mobile/lib/utils/diff.dart b/mobile/lib/utils/diff.dart index ea20de16cc..fae3a5d516 100644 --- a/mobile/lib/utils/diff.dart +++ b/mobile/lib/utils/diff.dart @@ -16,7 +16,8 @@ Future diffSortedLists( assert(la.isSorted(compare), "first argument must be sorted"); assert(lb.isSorted(compare), "second argument must be sorted"); bool diff = false; - int i = 0, j = 0; + int i = 0; + int j = 0; for (; i < la.length && j < lb.length;) { final int order = compare(la[i], lb[j]); if (order == 0) { @@ -53,7 +54,8 @@ bool diffSortedListsSync( assert(la.isSorted(compare), "first argument must be sorted"); assert(lb.isSorted(compare), "second argument must be sorted"); bool diff = false; - int i = 0, j = 0; + int i = 0; + int j = 0; for (; i < la.length && j < lb.length;) { final int order = compare(la[i], lb[j]); if (order == 0) { diff --git a/mobile/lib/utils/editor.utils.dart b/mobile/lib/utils/editor.utils.dart index fa2dedf383..c3440cce1a 100644 --- a/mobile/lib/utils/editor.utils.dart +++ b/mobile/lib/utils/editor.utils.dart @@ -50,10 +50,10 @@ typedef NormalizedTransform = ({double rotation, bool mirrorHorizontal, bool mir NormalizedTransform normalizeTransformEdits(List edits) { final matrix = buildAffineFromEdits(edits); - double a = matrix.a; - double b = matrix.b; - double c = matrix.c; - double d = matrix.d; + final double a = matrix.a; + final double b = matrix.b; + final double c = matrix.c; + final double d = matrix.d; final rotation = ((isCloseToZero(a) ? asin(c) : acos(a)) * 180) / pi; diff --git a/mobile/lib/utils/error_handler.dart b/mobile/lib/utils/error_handler.dart index d82e480575..dcac1431ab 100644 --- a/mobile/lib/utils/error_handler.dart +++ b/mobile/lib/utils/error_handler.dart @@ -24,7 +24,7 @@ void handleError(Object error, {StackTrace? stack, String? description}) { ); final String message; - if (serverErrorMessage(error) case String serverMessage) { + if (serverErrorMessage(error) case final String serverMessage) { message = serverMessage; } else if (isConnectionError(error)) { message = StaticTranslations.instance.login_form_server_error; diff --git a/mobile/lib/utils/hooks/crop_controller_hook.dart b/mobile/lib/utils/hooks/crop_controller_hook.dart index 663bca3dbf..cea901bd2e 100644 --- a/mobile/lib/utils/hooks/crop_controller_hook.dart +++ b/mobile/lib/utils/hooks/crop_controller_hook.dart @@ -1,7 +1,8 @@ -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:crop_image/crop_image.dart'; import 'dart:ui'; // Import the dart:ui library for Rect +import 'package:crop_image/crop_image.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + /// A hook that provides a [CropController] instance. CropController useCropController() { return useMemoized(() => CropController(defaultCrop: const Rect.fromLTRB(0, 0, 1, 1))); diff --git a/mobile/lib/utils/image_converter.dart b/mobile/lib/utils/image_converter.dart index 6711e2bd56..d0fd4f873f 100644 --- a/mobile/lib/utils/image_converter.dart +++ b/mobile/lib/utils/image_converter.dart @@ -12,7 +12,7 @@ import 'package:flutter/material.dart'; Future imageToUint8List(Image image) async { final Completer completer = Completer(); image.image - .resolve(const ImageConfiguration()) + .resolve(ImageConfiguration.empty) .addListener( ImageStreamListener((ImageInfo info, bool _) { info.image.toByteData(format: ImageByteFormat.png).then((byteData) { diff --git a/mobile/lib/utils/image_url_builder.dart b/mobile/lib/utils/image_url_builder.dart index 7a7ee03f2b..4f584061d7 100644 --- a/mobile/lib/utils/image_url_builder.dart +++ b/mobile/lib/utils/image_url_builder.dart @@ -12,7 +12,7 @@ String getThumbnailUrlForRemoteId( bool edited = true, String? thumbhash, }) { - final url = '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${type.toString()}&edited=$edited'; + final url = '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=$type&edited=$edited'; return thumbhash != null ? '$url&c=${Uri.encodeComponent(thumbhash)}' : url; } diff --git a/mobile/lib/utils/map_utils.dart b/mobile/lib/utils/map_utils.dart index 6213b214a9..19c66e51e9 100644 --- a/mobile/lib/utils/map_utils.dart +++ b/mobile/lib/utils/map_utils.dart @@ -68,7 +68,7 @@ class MapUtils { bool silent = false, }) async { try { - bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); + final bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled && !silent) { unawaited(showDialog(context: context, builder: (context) => _LocationServiceDisabledDialog())); return (null, LocationPermission.deniedForever); @@ -95,7 +95,7 @@ class MapUtils { return (null, LocationPermission.deniedForever); } - Position currentUserLocation = await Geolocator.getCurrentPosition( + final Position currentUserLocation = await Geolocator.getCurrentPosition( locationSettings: const LocationSettings( accuracy: LocationAccuracy.high, distanceFilter: 0, diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart index 711a4a3163..e92b2afd12 100644 --- a/mobile/lib/utils/openapi_patching.dart +++ b/mobile/lib/utils/openapi_patching.dart @@ -56,9 +56,9 @@ void upgradeDto(dynamic value, String targetType) { }); } -addDefault(dynamic value, String keys, dynamic defaultValue) { +void addDefault(dynamic value, String keys, dynamic defaultValue) { // Loop through the keys and assign the default value if the key is not present - List keyList = keys.split('.'); + final List keyList = keys.split('.'); dynamic current = value; for (int i = 0; i < keyList.length - 1; i++) { diff --git a/mobile/lib/utils/people.utils.dart b/mobile/lib/utils/people.utils.dart index ddd1867269..18ae8a4528 100644 --- a/mobile/lib/utils/people.utils.dart +++ b/mobile/lib/utils/people.utils.dart @@ -5,8 +5,8 @@ import 'package:immich_mobile/presentation/widgets/people/person_edit_birthday_m import 'package:immich_mobile/presentation/widgets/people/person_edit_name_modal.widget.dart'; String formatAge(DateTime birthDate, DateTime referenceDate) { - int ageInYears = _calculateAge(birthDate, referenceDate); - int ageInMonths = _calculateAgeInMonths(birthDate, referenceDate); + final int ageInYears = _calculateAge(birthDate, referenceDate); + final int ageInMonths = _calculateAgeInMonths(birthDate, referenceDate); if (ageInMonths <= 11) { return "person_age_months".t(args: {'months': ageInMonths.toString()}); diff --git a/mobile/lib/utils/timezone.dart b/mobile/lib/utils/timezone.dart index 3e8c42d1b2..980d51de8e 100644 --- a/mobile/lib/utils/timezone.dart +++ b/mobile/lib/utils/timezone.dart @@ -21,7 +21,7 @@ import 'package:timezone/timezone.dart'; return (dt, dt.timeZoneOffset); } on LocationNotFoundException { // Handle UTC offset format (e.g., "UTC+08:00") - RegExp re = RegExp(r'^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$', caseSensitive: false); + final RegExp re = RegExp(r'^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$', caseSensitive: false); final m = re.firstMatch(timeZone); if (m != null) { final hours = int.parse(m.group(1) ?? '0'); diff --git a/mobile/lib/widgets/album/remote_album_shared_user_icons.dart b/mobile/lib/widgets/album/remote_album_shared_user_icons.dart index 2025fa7583..be908259d4 100644 --- a/mobile/lib/widgets/album/remote_album_shared_user_icons.dart +++ b/mobile/lib/widgets/album/remote_album_shared_user_icons.dart @@ -30,12 +30,12 @@ class RemoteAlbumSharedUserIcons extends ConsumerWidget { height: 50, child: ListView.builder( scrollDirection: Axis.horizontal, - itemBuilder: ((context, index) { + itemBuilder: (context, index) { return Padding( padding: const EdgeInsets.only(right: 4.0), child: UserCircleAvatar(user: sharedUsers[index], size: 36, hasBorder: true), ); - }), + }, itemCount: sharedUsers.length, ), ), diff --git a/mobile/lib/widgets/asset_grid/thumbnail_placeholder.dart b/mobile/lib/widgets/asset_grid/thumbnail_placeholder.dart index a84dfbae37..12105aa498 100644 --- a/mobile/lib/widgets/asset_grid/thumbnail_placeholder.dart +++ b/mobile/lib/widgets/asset_grid/thumbnail_placeholder.dart @@ -11,7 +11,7 @@ class ThumbnailPlaceholder extends StatelessWidget { @override Widget build(BuildContext context) { - var gradientColors = [ + final gradientColors = [ context.colorScheme.surfaceContainer, context.colorScheme.surfaceContainer.darken(amount: .1), ]; diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart b/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart index f48ee06fdd..18bb27d024 100644 --- a/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart +++ b/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart @@ -39,7 +39,7 @@ class ExifMap extends StatelessWidget { const zoomLevel = 16; if (Platform.isAndroid) { - Uri uri = Uri( + final Uri uri = Uri( scheme: 'geo', host: '$latitude,$longitude', queryParameters: {'z': '$zoomLevel', 'q': '$latitude,$longitude'}, @@ -48,8 +48,8 @@ class ExifMap extends StatelessWidget { return uri; } } else if (Platform.isIOS) { - var params = {'ll': '$latitude,$longitude', 'q': '$latitude,$longitude', 'z': '$zoomLevel'}; - Uri uri = Uri.https('maps.apple.com', '/', params); + final params = {'ll': '$latitude,$longitude', 'q': '$latitude,$longitude', 'z': '$zoomLevel'}; + final Uri uri = Uri.https('maps.apple.com', '/', params); if (await canLaunchUrl(uri)) { return uri; } @@ -73,7 +73,7 @@ class ExifMap extends StatelessWidget { assetMarkerRemoteId: markerId, assetThumbhash: markerAssetThumbhash, onTap: (tapPosition, latLong) async { - Uri? uri = await createCoordinatesUri(); + final Uri? uri = await createCoordinatesUri(); if (uri == null) { return; diff --git a/mobile/lib/widgets/asset_viewer/video_controls.dart b/mobile/lib/widgets/asset_viewer/video_controls.dart index 0f1e0e020d..f39077a522 100644 --- a/mobile/lib/widgets/asset_viewer/video_controls.dart +++ b/mobile/lib/widgets/asset_viewer/video_controls.dart @@ -4,11 +4,11 @@ import 'package:async/async.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/colors.dart'; +import 'package:immich_mobile/extensions/duration_extensions.dart'; import 'package:immich_mobile/models/cast/cast_manager_state.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/extensions/duration_extensions.dart'; import 'package:immich_mobile/widgets/asset_viewer/animated_play_pause.dart'; class VideoControls extends ConsumerStatefulWidget { diff --git a/mobile/lib/widgets/backup/drift_album_info_list_tile.dart b/mobile/lib/widgets/backup/drift_album_info_list_tile.dart index 84128ddde2..85f655ec86 100644 --- a/mobile/lib/widgets/backup/drift_album_info_list_tile.dart +++ b/mobile/lib/widgets/backup/drift_album_info_list_tile.dart @@ -20,7 +20,7 @@ class DriftAlbumInfoListTile extends HookConsumerWidget { final bool isSelected = album.backupSelection == BackupSelection.selected; final bool isExcluded = album.backupSelection == BackupSelection.excluded; - buildTileColor() { + Color? buildTileColor() { if (isSelected) { return context.isDarkTheme ? context.primaryColor.withAlpha(100) : context.primaryColor.withAlpha(25); } else if (isExcluded) { @@ -30,7 +30,7 @@ class DriftAlbumInfoListTile extends HookConsumerWidget { } } - buildIcon() { + Icon buildIcon() { if (isSelected) { return Icon(Icons.check_circle_rounded, color: context.colorScheme.primary); } diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart index e77bc1869e..22c860becf 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart @@ -29,9 +29,9 @@ class ImmichAppBarDialog extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { ref.watch(localeProvider); - ServerDiskInfo backupState = ref.watch(backupProvider); + final ServerDiskInfo backupState = ref.watch(backupProvider); final theme = context.themeData; - bool isHorizontal = !context.isMobile; + final bool isHorizontal = !context.isMobile; final horizontalPadding = isHorizontal ? 100.0 : 20.0; final user = ref.watch(currentUserProvider); final isLoggingOut = useState(false); @@ -43,7 +43,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { return null; }, []); - buildTopRow() { + SizedBox buildTopRow() { return SizedBox( height: 56, child: Stack( @@ -68,7 +68,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ); } - buildActionButton(IconData icon, String text, Function() onTap, {Widget? trailing}) { + ListTile buildActionButton(IconData icon, String text, Function() onTap, {Widget? trailing}) { return ListTile( dense: true, visualDensity: VisualDensity.standard, @@ -84,11 +84,11 @@ class ImmichAppBarDialog extends HookConsumerWidget { ); } - buildSettingButton() { + ListTile buildSettingButton() { return buildActionButton(Icons.settings_outlined, "settings", () => context.pushRoute(const SettingsRoute())); } - buildFreeUpSpaceButton() { + ListTile buildFreeUpSpaceButton() { return buildActionButton( Icons.cleaning_services_outlined, "free_up_space", @@ -96,7 +96,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ); } - buildAppLogButton() { + ListTile buildAppLogButton() { return buildActionButton( Icons.assignment_outlined, "profile_drawer_app_logs", @@ -104,7 +104,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ); } - buildSignOutButton() { + ListTile buildSignOutButton() { return buildActionButton( Icons.logout_rounded, "sign_out", @@ -171,7 +171,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ); } - buildFooter() { + Padding buildFooter() { return Padding( padding: const EdgeInsets.only(top: 10, bottom: 20), child: Row( @@ -213,7 +213,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ); } - buildReadonlyMessage() { + Padding buildReadonlyMessage() { return Padding( padding: const EdgeInsets.only(left: 10.0, right: 10.0), child: ListTile( diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart index d6881f519a..c963564c69 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart @@ -24,7 +24,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget { final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); final user = ref.watch(currentUserProvider); - buildUserProfileImage() { + Widget buildUserProfileImage() { if (user == null) { return const CircleAvatar( radius: 20, @@ -42,17 +42,17 @@ class AppBarProfileInfoBox extends HookConsumerWidget { return userImage; } - pickUserProfileImage() async { + Future pickUserProfileImage() async { final XFile? image = await ImagePicker().pickImage(source: ImageSource.gallery, maxHeight: 1024, maxWidth: 1024); if (image != null) { - var success = await ref.watch(uploadProfileImageProvider.notifier).upload(image); + final success = await ref.watch(uploadProfileImageProvider.notifier).upload(image); if (success) { final profileImagePath = ref.read(uploadProfileImageProvider).profileImagePath; ref.watch(authProvider.notifier).updateUserProfileImagePath(profileImagePath); if (user != null) { - ref.read(currentUserProvider.notifier).refresh(); + unawaited(ref.read(currentUserProvider.notifier).refresh()); } unawaited(ref.read(backupProvider.notifier).updateDiskInfo()); diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart index a209d280c3..fbec03bbbd 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart @@ -18,14 +18,14 @@ class AppBarServerInfo extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { ref.watch(localeProvider); - ServerInfo serverInfoState = ref.watch(serverInfoProvider); + final ServerInfo serverInfoState = ref.watch(serverInfoProvider); final user = ref.watch(currentUserProvider); final bool showVersionWarning = ref.watch(versionWarningPresentProvider(user)); final appInfo = useState({}); - getPackageInfo() async { - PackageInfo packageInfo = await PackageInfo.fromPlatform(); + Future getPackageInfo() async { + final PackageInfo packageInfo = await PackageInfo.fromPlatform(); appInfo.value = {"version": packageInfo.version, "buildNumber": packageInfo.buildNumber}; } diff --git a/mobile/lib/widgets/common/app_bar_dialog/server_update_notification.dart b/mobile/lib/widgets/common/app_bar_dialog/server_update_notification.dart index 179eab8e7d..c29475351e 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/server_update_notification.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/server_update_notification.dart @@ -16,8 +16,10 @@ class ServerUpdateNotification extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final serverInfoState = ref.watch(serverInfoProvider); - Color errorColor = const Color.fromARGB(85, 253, 97, 83); - Color infoColor = context.isDarkTheme ? context.primaryColor.withAlpha(55) : context.primaryColor.withAlpha(25); + const Color errorColor = Color.fromARGB(85, 253, 97, 83); + final Color infoColor = context.isDarkTheme + ? context.primaryColor.withAlpha(55) + : context.primaryColor.withAlpha(25); void openUpdateLink() { String url; if (serverInfoState.versionStatus == VersionStatus.serverOutOfDate) { @@ -69,7 +71,7 @@ class ServerUpdateNotification extends HookConsumerWidget { onPressed: openUpdateLink, style: TextButton.styleFrom( padding: const EdgeInsets.all(4), - minimumSize: const Size(0, 0), + minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), child: serverInfoState.versionStatus == VersionStatus.clientOutOfDate diff --git a/mobile/lib/widgets/common/date_time_picker.dart b/mobile/lib/widgets/common/date_time_picker.dart index 0ebd7bba93..679241fc1b 100644 --- a/mobile/lib/widgets/common/date_time_picker.dart +++ b/mobile/lib/widgets/common/date_time_picker.dart @@ -41,7 +41,7 @@ class _DateTimePicker extends HookWidget { } } - Duration? tzOffset = initialTZOffset ?? initialDateTime?.timeZoneOffset; + final Duration? tzOffset = initialTZOffset ?? initialDateTime?.timeZoneOffset; if (tzOffset != null) { final offsetInMilli = tzOffset.inMilliseconds; @@ -80,7 +80,7 @@ class _DateTimePicker extends HookWidget { ) .toList(); - void pickDate() async { + Future pickDate() async { final now = DateTime.now(); // Handles cases where the date from the asset is far off in the future final initialDate = date.value.isAfter(now) ? now : date.value; diff --git a/mobile/lib/widgets/common/immich_sliver_app_bar.dart b/mobile/lib/widgets/common/immich_sliver_app_bar.dart index 6905b5b430..22528b05d5 100644 --- a/mobile/lib/widgets/common/immich_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/immich_sliver_app_bar.dart @@ -10,8 +10,8 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/sync_status.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; diff --git a/mobile/lib/widgets/common/immich_toast.dart b/mobile/lib/widgets/common/immich_toast.dart index 1da07f419d..0a06de4d91 100644 --- a/mobile/lib/widgets/common/immich_toast.dart +++ b/mobile/lib/widgets/common/immich_toast.dart @@ -5,7 +5,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; enum ToastType { info, success, error } class ImmichToast { - static show({ + static void show({ required BuildContext context, required String msg, ToastType toastType = ToastType.info, diff --git a/mobile/lib/widgets/common/person_sliver_app_bar.dart b/mobile/lib/widgets/common/person_sliver_app_bar.dart index a9f9413c47..80dded2130 100644 --- a/mobile/lib/widgets/common/person_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/person_sliver_app_bar.dart @@ -13,11 +13,11 @@ import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; -import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/utils/people.utils.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; +import 'package:immich_mobile/utils/people.utils.dart'; class PersonSliverAppBar extends ConsumerStatefulWidget { const PersonSliverAppBar({ @@ -56,8 +56,8 @@ class _MesmerizingSliverAppBarState extends ConsumerState { @override Widget build(BuildContext context) { final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled)); - Color? actionIconColor = Color.lerp(Colors.white, context.primaryColor, _scrollProgress); - List actionIconShadows = [ + final Color? actionIconColor = Color.lerp(Colors.white, context.primaryColor, _scrollProgress); + final List actionIconShadows = [ if (_scrollProgress < 0.95) Shadow(offset: const Offset(0, 2), blurRadius: 5, color: Colors.black.withValues(alpha: 0.5)) else diff --git a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart index fee77dcd99..4d2dc5ef88 100644 --- a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart @@ -64,9 +64,9 @@ class _MesmerizingSliverAppBarState extends ConsumerState actionIconShadows = [ + final List actionIconShadows = [ if (_scrollProgress < 0.95) Shadow(offset: const Offset(0, 2), blurRadius: 5, color: Colors.black.withValues(alpha: 0.5)) else diff --git a/mobile/lib/widgets/common/selection_sliver_app_bar.dart b/mobile/lib/widgets/common/selection_sliver_app_bar.dart index ac74e69e64..d60124041c 100644 --- a/mobile/lib/widgets/common/selection_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/selection_sliver_app_bar.dart @@ -23,7 +23,7 @@ class _SelectionSliverAppBarState extends ConsumerState { return !toExclude.contains(asset); }).toSet(); - onDone(Set selected) { + void onDone(Set selected) { ref.read(multiSelectProvider.notifier).reset(); context.pop>(selected); } diff --git a/mobile/lib/widgets/common/tag_picker.dart b/mobile/lib/widgets/common/tag_picker.dart index 0265cf7e6c..97fbff1930 100644 --- a/mobile/lib/widgets/common/tag_picker.dart +++ b/mobile/lib/widgets/common/tag_picker.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/tag.model.dart'; @@ -86,7 +86,7 @@ class TagPicker extends HookConsumerWidget { final searchQuery = useState(''); final tags = ref.watch(tagProvider); final selectedTagIds = useState>(filter); - final borderRadius = const BorderRadius.all(Radius.circular(10)); + const borderRadius = BorderRadius.all(Radius.circular(10)); final selectedNewTagValues = useState>({}); return Column( diff --git a/mobile/lib/widgets/forms/change_password_form.dart b/mobile/lib/widgets/forms/change_password_form.dart index 7ed9fa5f1c..7ab556b292 100644 --- a/mobile/lib/widgets/forms/change_password_form.dart +++ b/mobile/lib/widgets/forms/change_password_form.dart @@ -55,7 +55,7 @@ class ChangePasswordForm extends HookConsumerWidget { passwordController: passwordController, onPressed: () async { if (formKey.currentState!.validate()) { - var isSuccess = await ref + final isSuccess = await ref .read(authProvider.notifier) .changePassword(passwordController.value.text); diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index 79617f8fe4..4c9b56646f 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -76,7 +76,7 @@ class LoginForm extends HookConsumerWidget { final loginFormKey = GlobalKey(); final ValueNotifier serverEndpoint = useState(null); - checkVersionMismatch() async { + Future checkVersionMismatch() async { try { final packageInfo = await PackageInfo.fromPlatform(); final appSemVer = SemVer.fromString(packageInfo.version); @@ -151,13 +151,13 @@ class LoginForm extends HookConsumerWidget { return null; }, []); - populateTestLoginInfo() { + void populateTestLoginInfo() { emailController.text = 'demo@immich.app'; passwordController.text = 'demo'; serverEndpointController.text = 'https://demo.immich.app'; } - populateTestLoginInfo1() { + void populateTestLoginInfo1() { emailController.text = 'testuser@email.com'; passwordController.text = 'password'; serverEndpointController.text = 'http://10.1.15.216:2283/api'; @@ -177,7 +177,7 @@ class LoginForm extends HookConsumerWidget { } } - getManageMediaPermission() async { + Future getManageMediaPermission() async { final hasPermission = await ref.read(permissionRepositoryProvider).hasManageMediaPermission(); if (!hasPermission) { await showDialog( @@ -226,7 +226,7 @@ class LoginForm extends HookConsumerWidget { bool isSyncRemoteDeletionsMode() => Platform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false); - login() async { + Future login() async { TextInput.finishAutofillContext(); // Invalidate all api repository provider instance to take into account new access token @@ -277,13 +277,13 @@ class LoginForm extends HookConsumerWidget { } Future generatePKCECodeChallenge(String codeVerifier) async { - var bytes = utf8.encode(codeVerifier); - var digest = sha256.convert(bytes); + final bytes = utf8.encode(codeVerifier); + final digest = sha256.convert(bytes); return base64Url.encode(digest.bytes).replaceAll('=', ''); } - oAuthLogin() async { - var oAuthService = ref.watch(oAuthServiceProvider); + Future oAuthLogin() async { + final oAuthService = ref.watch(oAuthServiceProvider); String? oAuthServerUrl; final state = generateRandomString(32); @@ -357,7 +357,7 @@ class LoginForm extends HookConsumerWidget { } } - buildVersionCompatWarning() { + SingleChildRenderObjectWidget buildVersionCompatWarning() { checkVersionMismatch(); if (warningMessage.value == null) { diff --git a/mobile/lib/widgets/forms/pin_input.dart b/mobile/lib/widgets/forms/pin_input.dart index c4f0d8f3b7..8bcfd51282 100644 --- a/mobile/lib/widgets/forms/pin_input.dart +++ b/mobile/lib/widgets/forms/pin_input.dart @@ -26,9 +26,9 @@ class PinInput extends StatelessWidget { @override Widget build(BuildContext context) { - getPinSize() { - final minimumPadding = 18.0; - final gapWidth = 3.0; + Size getPinSize() { + const minimumPadding = 18.0; + const gapWidth = 3.0; final screenWidth = context.width; final pinWidth = (screenWidth - (minimumPadding * 2) - (gapWidth * 5)) / (length ?? 6); diff --git a/mobile/lib/widgets/forms/pin_registration_form.dart b/mobile/lib/widgets/forms/pin_registration_form.dart index d126169aad..b3270ec524 100644 --- a/mobile/lib/widgets/forms/pin_registration_form.dart +++ b/mobile/lib/widgets/forms/pin_registration_form.dart @@ -29,7 +29,7 @@ class PinRegistrationForm extends HookConsumerWidget { return true; } - createNewPinCode() async { + Future createNewPinCode() async { final isValid = validatePinCode(); if (!isValid) { hasError.value = true; diff --git a/mobile/lib/widgets/forms/pin_verification_form.dart b/mobile/lib/widgets/forms/pin_verification_form.dart index 2b7e3e8251..de2acaa1f7 100644 --- a/mobile/lib/widgets/forms/pin_verification_form.dart +++ b/mobile/lib/widgets/forms/pin_verification_form.dart @@ -29,7 +29,7 @@ class PinVerificationForm extends HookConsumerWidget { final hasError = useState(false); final isVerified = useState(false); - verifyPin(String pinCode) async { + Future verifyPin(String pinCode) async { final isUnlocked = await ref.read(authProvider.notifier).unlockPinCode(pinCode); if (isUnlocked) { diff --git a/mobile/lib/widgets/map/asset_marker_icon.dart b/mobile/lib/widgets/map/asset_marker_icon.dart index ff6058161b..75881195a4 100644 --- a/mobile/lib/widgets/map/asset_marker_icon.dart +++ b/mobile/lib/widgets/map/asset_marker_icon.dart @@ -69,15 +69,15 @@ class _PinPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - Paint primaryBrush = Paint() + final Paint primaryBrush = Paint() ..color = primaryColor ..style = PaintingStyle.fill; - Paint secondaryBrush = Paint() + final Paint secondaryBrush = Paint() ..color = secondaryColor ..style = PaintingStyle.fill; - Paint lineBrush = Paint() + final Paint lineBrush = Paint() ..color = primaryColor ..style = PaintingStyle.stroke ..strokeWidth = 2; diff --git a/mobile/lib/widgets/map/map_theme_override.dart b/mobile/lib/widgets/map/map_theme_override.dart index 57f970b0d1..dda96efeaf 100644 --- a/mobile/lib/widgets/map/map_theme_override.dart +++ b/mobile/lib/widgets/map/map_theme_override.dart @@ -66,7 +66,7 @@ class _MapThemeOverrideState extends ConsumerState with Widget @override Widget build(BuildContext context) { _theme = widget.themeMode ?? ref.watch(mapStateNotifierProvider.select((v) => v.themeMode)); - var appTheme = ref.watch(immichThemeProvider); + final appTheme = ref.watch(immichThemeProvider); final locale = ref.watch(localeProvider); useValueChanged(_theme, (_, __) { diff --git a/mobile/lib/widgets/map/map_thumbnail.dart b/mobile/lib/widgets/map/map_thumbnail.dart index 7defb52264..eb4653e17e 100644 --- a/mobile/lib/widgets/map/map_thumbnail.dart +++ b/mobile/lib/widgets/map/map_thumbnail.dart @@ -6,8 +6,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart'; -import 'package:immich_mobile/widgets/map/map_theme_override.dart'; import 'package:immich_mobile/widgets/map/asset_marker_icon.dart'; +import 'package:immich_mobile/widgets/map/map_theme_override.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; /// A non-interactive thumbnail of a map in the given coordinates with optional markers diff --git a/mobile/lib/widgets/photo_view/src/core/photo_view_core.dart b/mobile/lib/widgets/photo_view/src/core/photo_view_core.dart index 265feb756e..341decdbd6 100644 --- a/mobile/lib/widgets/photo_view/src/core/photo_view_core.dart +++ b/mobile/lib/widgets/photo_view/src/core/photo_view_core.dart @@ -1,15 +1,15 @@ import 'package:flutter/widgets.dart'; import 'package:immich_mobile/widgets/photo_view/photo_view.dart' show - PhotoViewScaleState, PhotoViewHeroAttributes, - PhotoViewImageTapDownCallback, - PhotoViewImageTapUpCallback, - PhotoViewImageScaleEndCallback, PhotoViewImageDragEndCallback, PhotoViewImageDragStartCallback, PhotoViewImageDragUpdateCallback, PhotoViewImageLongPressStartCallback, + PhotoViewImageScaleEndCallback, + PhotoViewImageTapDownCallback, + PhotoViewImageTapUpCallback, + PhotoViewScaleState, ScaleStateCycle; import 'package:immich_mobile/widgets/photo_view/src/controller/photo_view_controller.dart'; import 'package:immich_mobile/widgets/photo_view/src/controller/photo_view_controller_delegate.dart'; @@ -436,7 +436,7 @@ class PhotoViewCoreState extends State ? SizedBox( width: scaleBoundaries.childSize.width * scale, height: scaleBoundaries.childSize.height * scale, - child: widget.customChild!, + child: widget.customChild, ) : Image( key: widget.heroAttributes?.tag != null ? ObjectKey(widget.heroAttributes!.tag) : null, diff --git a/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart b/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart index a9cfeb3a40..db66cb962d 100644 --- a/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart +++ b/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart @@ -75,7 +75,7 @@ class ImageWrapper extends StatefulWidget { final int index; @override - createState() => _ImageWrapperState(); + State createState() => _ImageWrapperState(); } class _ImageWrapperState extends State { @@ -122,7 +122,7 @@ class _ImageWrapperState extends State { // retrieve image from the provider void _resolveImage() { - final ImageStream newStream = widget.imageProvider.resolve(const ImageConfiguration()); + final ImageStream newStream = widget.imageProvider.resolve(ImageConfiguration.empty); _updateSourceStream(newStream); } @@ -135,7 +135,7 @@ class _ImageWrapperState extends State { } void handleImageFrame(ImageInfo info, bool synchronousCall) { - setupCB() { + void setupCB() { _imageSize = Size(info.image.width.toDouble(), info.image.height.toDouble()); _loading = false; _imageInfo = _imageInfo; diff --git a/mobile/lib/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart b/mobile/lib/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart index dee42ec5a0..1c338b8828 100644 --- a/mobile/lib/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart +++ b/mobile/lib/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart @@ -20,7 +20,7 @@ class FilterBottomSheetScaffold extends StatelessWidget { @override Widget build(BuildContext context) { - buildChildWidget() { + Widget buildChildWidget() { if (expanded != null && expanded == true) { return Expanded(child: child); } diff --git a/mobile/lib/widgets/search/search_filter/people_picker.dart b/mobile/lib/widgets/search/search_filter/people_picker.dart index a9382ec3ae..0732b30a63 100644 --- a/mobile/lib/widgets/search/search_filter/people_picker.dart +++ b/mobile/lib/widgets/search/search_filter/people_picker.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/person.model.dart'; @@ -21,7 +21,7 @@ class PeoplePicker extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final formFocus = useFocusNode(); - final imageSize = 60.0; + const imageSize = 60.0; final searchQuery = useState(''); final people = ref.watch(getAllPeopleProvider); final selectedPeople = useState>(filter ?? {}); diff --git a/mobile/lib/widgets/search/search_filter/star_rating_picker.dart b/mobile/lib/widgets/search/search_filter/star_rating_picker.dart index 32d1ab5bd4..864ddf2591 100644 --- a/mobile/lib/widgets/search/search_filter/star_rating_picker.dart +++ b/mobile/lib/widgets/search/search_filter/star_rating_picker.dart @@ -28,7 +28,7 @@ class StarRatingPicker extends HookWidget { 6, (index) => RadioListTile( key: Key("star_$index"), - title: Text('rating_count'.t(args: {'count': (index)})), + title: Text('rating_count'.t(args: {'count': index})), value: index, ), ), diff --git a/mobile/lib/widgets/search/thumbnail_with_info.dart b/mobile/lib/widgets/search/thumbnail_with_info.dart index 7ba8257c8a..d25a324860 100644 --- a/mobile/lib/widgets/search/thumbnail_with_info.dart +++ b/mobile/lib/widgets/search/thumbnail_with_info.dart @@ -22,7 +22,7 @@ class ThumbnailWithInfo extends StatelessWidget { @override Widget build(BuildContext context) { - var textAndIconColor = context.isDarkTheme ? Colors.grey[100] : Colors.grey[700]; + final textAndIconColor = context.isDarkTheme ? Colors.grey[100] : Colors.grey[700]; return ThumbnailWithInfoContainer( onTap: onTap, borderRadius: borderRadius, diff --git a/mobile/lib/widgets/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index 542a7cc5e2..1c1d42639f 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -7,9 +7,9 @@ import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/repositories/permission.repository.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; @@ -45,9 +45,9 @@ class AdvancedSettings extends HookConsumerWidget { Future checkAndroidVersion() async { if (Platform.isAndroid) { - DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); - AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; - int sdkVersion = androidInfo.version.sdkInt; + final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + final AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + final int sdkVersion = androidInfo.version.sdkInt; return sdkVersion >= 31; } return false; diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart b/mobile/lib/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart index f3b9039b2b..25829327e0 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart'; import 'package:immich_mobile/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart'; -import 'package:immich_mobile/widgets/settings/asset_viewer_settings/video_viewer_settings.dart'; import 'package:immich_mobile/widgets/settings/asset_viewer_settings/slideshow_settings.dart'; +import 'package:immich_mobile/widgets/settings/asset_viewer_settings/video_viewer_settings.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; class AssetViewerSettings extends StatelessWidget { diff --git a/mobile/lib/widgets/settings/free_up_space_settings.dart b/mobile/lib/widgets/settings/free_up_space_settings.dart index da14933997..7b16c2d67d 100644 --- a/mobile/lib/widgets/settings/free_up_space_settings.dart +++ b/mobile/lib/widgets/settings/free_up_space_settings.dart @@ -173,7 +173,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { } @override - dispose() { + void dispose() { super.dispose(); WakelockPlus.disable(); } diff --git a/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart index 8250ef438a..f3c2b6c97f 100644 --- a/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart +++ b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart @@ -19,7 +19,7 @@ class ExternalNetworkPreference extends HookConsumerWidget { final entries = useState([const AuxilaryEndpoint(url: '', status: AuxCheckStatus.unknown)]); final canSave = useState(false); - saveEndpointList() { + Future saveEndpointList() { canSave.value = entries.value.every((e) => e.status == AuxCheckStatus.valid); final urls = entries.value @@ -30,7 +30,7 @@ class ExternalNetworkPreference extends HookConsumerWidget { return ref.read(settingsProvider).write(SettingsKey.networkExternalEndpointList, urls); } - updateValidationStatus(String url, int index, AuxCheckStatus status) async { + Future updateValidationStatus(String url, int index, AuxCheckStatus status) async { entries.value[index] = entries.value[index].copyWith(url: url, status: status); await saveEndpointList(); @@ -39,7 +39,7 @@ class ExternalNetworkPreference extends HookConsumerWidget { } } - handleReorder(int oldIndex, int newIndex) { + void handleReorder(int oldIndex, int newIndex) { final entry = entries.value.removeAt(oldIndex); entries.value.insert(newIndex, entry); entries.value = [...entries.value]; @@ -47,7 +47,7 @@ class ExternalNetworkPreference extends HookConsumerWidget { saveEndpointList(); } - handleDismiss(int index) { + void handleDismiss(int index) { entries.value = [...entries.value..removeAt(index)]; saveEndpointList(); diff --git a/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart b/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart index 1d2b5eea0f..f8b6b087a3 100644 --- a/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart +++ b/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart @@ -62,17 +62,17 @@ class LocalNetworkPreference extends HookConsumerWidget { return null; }, []); - saveWifiName(String wifiName) { + Future saveWifiName(String wifiName) { wifiNameText.value = wifiName; return ref.read(authProvider.notifier).saveWifiName(wifiName); } - saveLocalEndpoint(String url) { + Future saveLocalEndpoint(String url) { localEndpointText.value = url; return ref.read(authProvider.notifier).saveLocalEndpoint(url); } - handleEditWifiName() async { + Future handleEditWifiName() async { final wifiName = await _showEditDialog(context, "wifi_name".tr(), "your_wifi_name".tr(), wifiNameText.value); if (wifiName != null) { @@ -80,7 +80,7 @@ class LocalNetworkPreference extends HookConsumerWidget { } } - handleEditServerEndpoint() async { + Future handleEditServerEndpoint() async { final localEndpoint = await _showEditDialog( context, "server_endpoint".tr(), @@ -94,7 +94,7 @@ class LocalNetworkPreference extends HookConsumerWidget { } } - autofillCurrentNetwork() async { + Future autofillCurrentNetwork() async { final wifiName = await ref.read(networkProvider.notifier).getWifiName(); if (wifiName == null) { diff --git a/mobile/lib/widgets/settings/notification_setting.dart b/mobile/lib/widgets/settings/notification_setting.dart index 8b7c652925..ee2e15f52b 100644 --- a/mobile/lib/widgets/settings/notification_setting.dart +++ b/mobile/lib/widgets/settings/notification_setting.dart @@ -15,14 +15,14 @@ class NotificationSetting extends HookConsumerWidget { final permissionService = ref.watch(notificationPermissionProvider); final hasPermission = permissionService == PermissionStatus.granted; - openAppNotificationSettings(BuildContext ctx) { + void openAppNotificationSettings(BuildContext ctx) { ctx.pop(); openAppSettings(); } // When permissions are permanently denied, you need to go to settings to // allow them - showPermissionsDialog() { + void showPermissionsDialog() { showDialog( context: context, builder: (ctx) => AlertDialog( diff --git a/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart b/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart index 5e745dd61d..bfdb0f065c 100644 --- a/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/haptic_setting.dart @@ -3,9 +3,9 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; -import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; class HapticSetting extends HookConsumerWidget { const HapticSetting({super.key}); @@ -15,7 +15,7 @@ class HapticSetting extends HookConsumerWidget { final hapticFeedbackSetting = useAppSettingsState(AppSettingsEnum.enableHapticFeedback); final isHapticFeedbackEnabled = useValueNotifier(hapticFeedbackSetting.value); - onHapticFeedbackChange(bool isEnabled) { + void onHapticFeedbackChange(bool isEnabled) { hapticFeedbackSetting.value = isEnabled; } diff --git a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart index 48d0ca672b..3fead2c59f 100644 --- a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart @@ -25,12 +25,12 @@ class PrimaryColorSetting extends HookConsumerWidget { }); } - onUseSystemColorChange(bool newValue) { + void onUseSystemColorChange(bool newValue) { ref.read(settingsProvider).write(.themeDynamic, newValue); popBottomSheet(); } - onPrimaryColorChange(ImmichColorPreset colorPreset) { + void onPrimaryColorChange(ImmichColorPreset colorPreset) { ref.read(settingsProvider).write(.themePrimaryColor, colorPreset); //turn off system color setting @@ -40,7 +40,7 @@ class PrimaryColorSetting extends HookConsumerWidget { popBottomSheet(); } - buildPrimaryColorTile({ + Container buildPrimaryColorTile({ required Color topColor, required Color bottomColor, required double tileSize, @@ -85,7 +85,7 @@ class PrimaryColorSetting extends HookConsumerWidget { ); } - bottomSheetContent() { + Column bottomSheetContent() { return Column( mainAxisSize: MainAxisSize.min, children: [ diff --git a/mobile/lib/widgets/settings/settings_switch_list_tile.dart b/mobile/lib/widgets/settings/settings_switch_list_tile.dart index d8ed3ac017..4d358fa2b6 100644 --- a/mobile/lib/widgets/settings/settings_switch_list_tile.dart +++ b/mobile/lib/widgets/settings/settings_switch_list_tile.dart @@ -44,7 +44,7 @@ class SettingsSwitchListTile extends StatelessWidget { onChanged: onSwitchChanged, activeThumbColor: enabled ? context.primaryColor : context.themeData.disabledColor, dense: true, - secondary: icon != null ? Icon(icon!, color: valueNotifier.value ? context.primaryColor : null) : null, + secondary: icon != null ? Icon(icon, color: valueNotifier.value ? context.primaryColor : null) : null, title: Text( title, style: diff --git a/mobile/packages/ui/lib/src/components/password_input.dart b/mobile/packages/ui/lib/src/components/password_input.dart index e99e9730a8..d08b3de99d 100644 --- a/mobile/packages/ui/lib/src/components/password_input.dart +++ b/mobile/packages/ui/lib/src/components/password_input.dart @@ -51,7 +51,7 @@ class _ImmichPasswordInputState extends State { onPressed: _toggleVisibility, icon: Icon(_visible ? Icons.visibility_off_rounded : Icons.visibility_rounded), ), - autofillHints: [AutofillHints.password], + autofillHints: const [AutofillHints.password], ); } } diff --git a/mobile/test/domain/services/sync_stream_service_test.dart b/mobile/test/domain/services/sync_stream_service_test.dart index e033229408..ac81513d96 100644 --- a/mobile/test/domain/services/sync_stream_service_test.dart +++ b/mobile/test/domain/services/sync_stream_service_test.dart @@ -70,7 +70,7 @@ void main() { await db.close(); }); - successHandler(Invocation _) async => true; + Future successHandler(Invocation _) async => true; setUp(() async { mockSyncStreamRepo = MockSyncStreamRepository(); diff --git a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart index d538b567bd..3a3e40cbae 100644 --- a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart +++ b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart @@ -37,7 +37,7 @@ void main() { late MockHttpClient mockHttpClient; late MockStreamedResponse mockStreamedResponse; late StreamController> responseStreamController; - late int testBatchSize = 3; + const int testBatchSize = 3; setUpAll(() async { final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); diff --git a/mobile/test/infrastructure/repository.mock.dart b/mobile/test/infrastructure/repository.mock.dart index 0688576682..355e128fc0 100644 --- a/mobile/test/infrastructure/repository.mock.dart +++ b/mobile/test/infrastructure/repository.mock.dart @@ -3,9 +3,9 @@ import 'package:immich_mobile/infrastructure/repositories/local_album.repository import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/partner.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; diff --git a/mobile/test/medium/repositories/local_asset_repository_test.dart b/mobile/test/medium/repositories/local_asset_repository_test.dart index bc74728346..d92b1c0184 100644 --- a/mobile/test/medium/repositories/local_asset_repository_test.dart +++ b/mobile/test/medium/repositories/local_asset_repository_test.dart @@ -456,7 +456,7 @@ void main() { test('does not update when longitude does not match', () async { final remoteAsset = await ctx.newRemoteAsset(ownerId: userId); - final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id, longitude: .fromNullable((-74.006))); + final cloudIdAsset = await ctx.newRemoteAssetCloudId(id: remoteAsset.id, longitude: .fromNullable(-74.006)); final localAsset = await ctx.newLocalAsset( checksumOption: const Option.none(), iCloudId: cloudIdAsset.cloudId, diff --git a/mobile/test/medium/repositories/timeline_repository_test.dart b/mobile/test/medium/repositories/timeline_repository_test.dart index 94b3413c9b..d78d9b1ef7 100644 --- a/mobile/test/medium/repositories/timeline_repository_test.dart +++ b/mobile/test/medium/repositories/timeline_repository_test.dart @@ -27,7 +27,7 @@ void main() { // Regression check for #23273: a LEFT OUTER JOIN on checksum would fan out and create duplicates // happens when same photo exists in multiple albums on device final user = await ctx.newUser(); - final checksum = 'yolo'; + const checksum = 'yolo'; final album = await ctx.newRemoteAlbum(ownerId: user.id); final remoteAsset = await ctx.newRemoteAsset(ownerId: user.id, checksum: checksum); await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: remoteAsset.id); diff --git a/mobile/test/modules/utils/async_mutex_test.dart b/mobile/test/modules/utils/async_mutex_test.dart index 08cafeb307..10fdcbad5e 100644 --- a/mobile/test/modules/utils/async_mutex_test.dart +++ b/mobile/test/modules/utils/async_mutex_test.dart @@ -6,8 +6,8 @@ import 'package:immich_mobile/utils/async_mutex.dart'; void main() { group('Test AsyncMutex grouped', () { test('test ordered execution', () async { - AsyncMutex lock = AsyncMutex(); - List events = []; + final AsyncMutex lock = AsyncMutex(); + final List events = []; expect(0, lock.enqueued); unawaited(lock.run(() => Future.delayed(const Duration(milliseconds: 10), () => events.add(1)))); expect(1, lock.enqueued); diff --git a/mobile/test/modules/utils/datetime_helpers_test.dart b/mobile/test/modules/utils/datetime_helpers_test.dart index dfe83b4925..ca196224a0 100644 --- a/mobile/test/modules/utils/datetime_helpers_test.dart +++ b/mobile/test/modules/utils/datetime_helpers_test.dart @@ -10,38 +10,38 @@ void main() { test('returns null for value below minimum allowed range', () { // _minMillisecondsSinceEpoch = -62135596800000 - final seconds = -62135596800000 ~/ 1000 - 1; // One second before min allowed + const seconds = -62135596800000 ~/ 1000 - 1; // One second before min allowed final result = tryFromSecondsSinceEpoch(seconds); expect(result, isNull); }); test('returns null for value above maximum allowed range', () { // _maxMillisecondsSinceEpoch = 8640000000000000 - final seconds = 8640000000000000 ~/ 1000 + 1; // One second after max allowed + const seconds = 8640000000000000 ~/ 1000 + 1; // One second after max allowed final result = tryFromSecondsSinceEpoch(seconds); expect(result, isNull); }); test('returns correct DateTime for minimum allowed value', () { - final seconds = -62135596800000 ~/ 1000; // Minimum allowed timestamp + const seconds = -62135596800000 ~/ 1000; // Minimum allowed timestamp final result = tryFromSecondsSinceEpoch(seconds); expect(result, DateTime.fromMillisecondsSinceEpoch(-62135596800000)); }); test('returns correct DateTime for maximum allowed value', () { - final seconds = 8640000000000000 ~/ 1000; // Maximum allowed timestamp + const seconds = 8640000000000000 ~/ 1000; // Maximum allowed timestamp final result = tryFromSecondsSinceEpoch(seconds); expect(result, DateTime.fromMillisecondsSinceEpoch(8640000000000000)); }); test('returns correct DateTime for negative timestamp', () { - final seconds = -1577836800; // Dec 31, 1919 (pre-epoch) + const seconds = -1577836800; // Dec 31, 1919 (pre-epoch) final result = tryFromSecondsSinceEpoch(seconds); expect(result, DateTime.fromMillisecondsSinceEpoch(-1577836800 * 1000)); }); test('returns correct DateTime for zero timestamp', () { - final seconds = 0; // Jan 1, 1970 (epoch) + const seconds = 0; // Jan 1, 1970 (epoch) final result = tryFromSecondsSinceEpoch(seconds); expect(result, DateTime.fromMillisecondsSinceEpoch(0)); }); diff --git a/mobile/test/modules/utils/debouncer_test.dart b/mobile/test/modules/utils/debouncer_test.dart index 7aa13842d6..036c538f03 100644 --- a/mobile/test/modules/utils/debouncer_test.dart +++ b/mobile/test/modules/utils/debouncer_test.dart @@ -11,7 +11,7 @@ class _Counter { void main() { test('Executes the method after the interval', () async { - var counter = _Counter(); + final counter = _Counter(); final debouncer = Debouncer(interval: const Duration(milliseconds: 300)); debouncer.run(() => counter.increment()); expect(counter.count, 0); @@ -20,17 +20,17 @@ void main() { }); test('Executes the method immediately if zero interval', () async { - var counter = _Counter(); - final debouncer = Debouncer(interval: const Duration(milliseconds: 0)); + final counter = _Counter(); + final debouncer = Debouncer(interval: Duration.zero); debouncer.run(() => counter.increment()); // Even though it is supposed to be executed immediately, it is added to the async queue and so // we need this delay to make sure the actual debounced method is called - await Future.delayed(const Duration(milliseconds: 0)); + await Future.delayed(Duration.zero); expect(counter.count, 1); }); test('Delayes method execution after all the calls are completed', () async { - var counter = _Counter(); + final counter = _Counter(); final debouncer = Debouncer(interval: const Duration(milliseconds: 100)); debouncer.run(() => counter.increment()); debouncer.run(() => counter.increment()); diff --git a/mobile/test/modules/utils/openapi_patching_test.dart b/mobile/test/modules/utils/openapi_patching_test.dart index 18ab07b3a9..15d42c34f2 100644 --- a/mobile/test/modules/utils/openapi_patching_test.dart +++ b/mobile/test/modules/utils/openapi_patching_test.dart @@ -1,8 +1,8 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; -import 'package:openapi/api.dart'; import 'package:immich_mobile/utils/openapi_patching.dart'; +import 'package:openapi/api.dart'; void main() { group('Test OpenApi Patching', () { @@ -26,7 +26,7 @@ void main() { }); test('addDefault', () { - dynamic value = jsonDecode(""" + final dynamic value = jsonDecode(""" { "download": { "archiveSize": 4294967296, @@ -47,7 +47,7 @@ void main() { }); test('addDefault with null', () { - dynamic value = jsonDecode(""" + final dynamic value = jsonDecode(""" { "download": { "archiveSize": 4294967296, diff --git a/mobile/test/services/auth.service_test.dart b/mobile/test/services/auth.service_test.dart index d71a52f2ae..b62ad3ad2c 100644 --- a/mobile/test/services/auth.service_test.dart +++ b/mobile/test/services/auth.service_test.dart @@ -103,7 +103,7 @@ void main() { }); test('Should return null if auto endpoint switching is disabled', () async { - when(() => authRepository.getEndpointSwitchingFeature()).thenReturn((false)); + when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(false); final result = await sut.setOpenApiServiceEndpoint(); diff --git a/mobile/test/services/background_upload.service_test.dart b/mobile/test/services/background_upload.service_test.dart index 65f17501a0..aa46bd9de3 100644 --- a/mobile/test/services/background_upload.service_test.dart +++ b/mobile/test/services/background_upload.service_test.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'dart:io'; -import 'package:drift/drift.dart' hide isNull, isNotNull; +import 'package:drift/drift.dart' hide isNotNull, isNull; import 'package:drift/native.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; diff --git a/mobile/test/services/foreground_upload.service_test.dart b/mobile/test/services/foreground_upload.service_test.dart index 46e9a82141..d4344ed77e 100644 --- a/mobile/test/services/foreground_upload.service_test.dart +++ b/mobile/test/services/foreground_upload.service_test.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:drift/drift.dart' hide isNull, isNotNull; +import 'package:drift/drift.dart' hide isNotNull, isNull; import 'package:drift/native.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/mobile/test/unit/presentation/partner_page_test.dart b/mobile/test/unit/presentation/partner_page_test.dart index 162094c720..ee9c6a3575 100644 --- a/mobile/test/unit/presentation/partner_page_test.dart +++ b/mobile/test/unit/presentation/partner_page_test.dart @@ -17,7 +17,7 @@ void main() { group('PartnerSharedByList', () { testWidgets('shows the empty-state add button when there are no partners', (tester) async { - final action = const PartnerAddAction(); + const action = PartnerAddAction(); await tester.pumpTestWidget(context, const PartnerSharedByList(partners: [])); @@ -39,7 +39,7 @@ void main() { testWidgets('renders a remove action for each partner', (tester) async { final partner1 = PartnerFactory.create(inTimeline: true); final partner2 = PartnerFactory.create(); - final action = const PartnerRemoveAction(sharedWithId: '', partnerName: ''); + const action = PartnerRemoveAction(sharedWithId: '', partnerName: ''); await tester.pumpTestWidget(context, PartnerSharedByList(partners: [partner1, partner2])); expect(find.byIcon(action.icon), findsNWidgets(2)); }); diff --git a/mobile/test/unit/utils/editor_test.dart b/mobile/test/unit/utils/editor_test.dart index 82cf584f76..6f3f41f374 100644 --- a/mobile/test/unit/utils/editor_test.dart +++ b/mobile/test/unit/utils/editor_test.dart @@ -4,7 +4,7 @@ import 'package:immich_mobile/utils/editor.utils.dart'; import 'package:openapi/api.dart' show MirrorAxis, MirrorParameters, RotateParameters; List normalizedToEdits(NormalizedTransform transform) { - List edits = []; + final List edits = []; if (transform.mirrorHorizontal) { edits.add(MirrorEdit(MirrorParameters(axis: MirrorAxis.horizontal))); @@ -25,10 +25,10 @@ bool compareEditAffines(List editsA, List editsB) { final normA = buildAffineFromEdits(editsA); final normB = buildAffineFromEdits(editsB); - return ((normA.a - normB.a).abs() < 0.0001 && + return (normA.a - normB.a).abs() < 0.0001 && (normA.b - normB.b).abs() < 0.0001 && (normA.c - normB.c).abs() < 0.0001 && - (normA.d - normB.d).abs() < 0.0001); + (normA.d - normB.d).abs() < 0.0001; } void main() { diff --git a/mobile/test/utils_legacy/action_button_utils_test.dart b/mobile/test/utils_legacy/action_button_utils_test.dart index 0a6020762a..52d25c4c75 100644 --- a/mobile/test/utils_legacy/action_button_utils_test.dart +++ b/mobile/test/utils_legacy/action_button_utils_test.dart @@ -1074,7 +1074,7 @@ void main() { test('should build correct widget for each button type', () { for (final buttonType in ActionButtonType.values) { - var buttonContext = context; + final buttonContext = context; if (buttonType == ActionButtonType.removeFromAlbum) { final album = createRemoteAlbum(); From 9d03a92b10c0d46c08c1fd2f94c1176235ef65b8 Mon Sep 17 00:00:00 2001 From: Ben Beckford Date: Thu, 30 Jul 2026 02:08:24 -0700 Subject: [PATCH 02/81] fix: assetFileFilter path matching (#30394) --- packages/plugin-core/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/plugin-core/src/index.ts b/packages/plugin-core/src/index.ts index e3f180f98b..7b91ed7111 100644 --- a/packages/plugin-core/src/index.ts +++ b/packages/plugin-core/src/index.ts @@ -89,7 +89,8 @@ const methods = wrapper({ } }, - assetFileFilter: ({ data, config }) => matchValueResult(data.asset.originalFileName || '', config), + assetFileFilter: ({ data, config }) => + matchValueResult(config.usePath ? data.asset.originalPath : data.asset.originalFileName, config), assetLocationFilter: ({ config, data }) => { if ( From 7e70f90c15466cfd709ceed6442df18b03912716 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:53:08 +0530 Subject: [PATCH 03/81] fix: apply disabled styling to UI buttons (#30322) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../ui/lib/src/components/column_button.dart | 17 +++-------------- .../ui/lib/src/components/icon_button.dart | 17 +++-------------- .../ui/lib/src/components/text_button.dart | 17 +++-------------- 3 files changed, 9 insertions(+), 42 deletions(-) diff --git a/mobile/packages/ui/lib/src/components/column_button.dart b/mobile/packages/ui/lib/src/components/column_button.dart index f990dbc065..03b6933389 100644 --- a/mobile/packages/ui/lib/src/components/column_button.dart +++ b/mobile/packages/ui/lib/src/components/column_button.dart @@ -43,21 +43,10 @@ class _ImmichColumnButtonState extends State { } } - Future? _onPressed() { - if (_isDisabled) { - return null; - } + VoidCallback? get _onPressed => _isDisabled ? null : () => _runAction(widget.onPressed); - return _runAction(widget.onPressed); - } - - Future? _onLongPress() { - if (_isDisabled || widget.onLongPress == null) { - return null; - } - - return _runAction(widget.onLongPress!); - } + VoidCallback? get _onLongPress => + _isDisabled || widget.onLongPress == null ? null : () => _runAction(widget.onLongPress!); @override Widget build(BuildContext context) { diff --git a/mobile/packages/ui/lib/src/components/icon_button.dart b/mobile/packages/ui/lib/src/components/icon_button.dart index 1c02f30ed3..c0e1bcc3fa 100644 --- a/mobile/packages/ui/lib/src/components/icon_button.dart +++ b/mobile/packages/ui/lib/src/components/icon_button.dart @@ -44,21 +44,10 @@ class _ImmichIconButtonState extends State { } } - Future? _onPressed() { - if (_isDisabled) { - return null; - } + VoidCallback? get _onPressed => _isDisabled ? null : () => _runAction(widget.onPressed); - return _runAction(widget.onPressed); - } - - Future? _onLongPress() { - if (_isDisabled || widget.onLongPress == null) { - return null; - } - - return _runAction(widget.onLongPress!); - } + VoidCallback? get _onLongPress => + _isDisabled || widget.onLongPress == null ? null : () => _runAction(widget.onLongPress!); @override Widget build(BuildContext context) { diff --git a/mobile/packages/ui/lib/src/components/text_button.dart b/mobile/packages/ui/lib/src/components/text_button.dart index bbcfe4a3a7..6cbc08473b 100644 --- a/mobile/packages/ui/lib/src/components/text_button.dart +++ b/mobile/packages/ui/lib/src/components/text_button.dart @@ -46,21 +46,10 @@ class _ImmichTextButtonState extends State { } } - Future? _onPressed() { - if (_isDisabled) { - return null; - } + VoidCallback? get _onPressed => _isDisabled ? null : () => _runAction(widget.onPressed); - return _runAction(widget.onPressed); - } - - Future? _onLongPress() { - if (_isDisabled || widget.onLongPress == null) { - return null; - } - - return _runAction(widget.onLongPress!); - } + VoidCallback? get _onLongPress => + _isDisabled || widget.onLongPress == null ? null : () => _runAction(widget.onLongPress!); @override Widget build(BuildContext context) { From 1f16fe16c2b0c4fea25de23b1a2dbe1b606f4551 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 30 Jul 2026 17:58:08 +0200 Subject: [PATCH 04/81] chore: enable merge queue batching (#30410) This waits for up to 2 min to group up to 3 PRs into the same draft. That should cut down on the amount of draft PRs that get created. The main downsides are the 2 minute wait when only queueing one PR, and less failure isolation: on an error, mergify has to do bisect runs to figure out which PR failed the batch, which takes more time than purely sequential operation. All of this can be tuned further of course. --- .mergify.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.mergify.yml b/.mergify.yml index 226e26c4c0..12f3ef6715 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -1,2 +1,7 @@ merge_queue: status_comments: outcomes + +queue_rules: + - name: default + batch_size: 3 + batch_max_wait_time: 2 min From a316ba35cafe0d22f9b414bb9ed4d64a79ba4b37 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 30 Jul 2026 21:12:25 +0200 Subject: [PATCH 05/81] fix: shared check for server setup availability (#30311) * fix: shared check for server setup availability * chore: add medium test * feat: require @Authenticated decorator everywhere * fix: lints --- docs/docs/install/environment-variables.md | 2 +- e2e/src/responses.ts | 3 - .../server/database-backups.e2e-spec.ts | 2 +- server/src/controllers/app.controller.ts | 3 + .../src/controllers/auth.controller.spec.ts | 13 +++ server/src/controllers/auth.controller.ts | 2 + .../database-backup.controller.spec.ts | 55 +++++++++++++ .../controllers/database-backup.controller.ts | 1 + server/src/controllers/index.spec.ts | 8 +- .../src/controllers/maintenance.controller.ts | 2 + server/src/controllers/oauth.controller.ts | 4 + server/src/controllers/server.controller.ts | 6 ++ server/src/middleware/auth.guard.spec.ts | 82 +++++++++++++++++++ server/src/middleware/auth.guard.ts | 27 ++++-- server/src/services/auth.service.spec.ts | 10 --- server/src/services/auth.service.ts | 10 --- server/src/services/base.service.ts | 11 +++ .../src/services/maintenance.service.spec.ts | 16 ++++ server/src/services/maintenance.service.ts | 7 +- server/src/services/server.service.spec.ts | 16 +++- server/src/services/server.service.ts | 3 +- server/test/medium/responses.ts | 3 - .../specs/services/auth.service.spec.ts | 10 --- server/test/utils.ts | 19 +++-- 24 files changed, 249 insertions(+), 66 deletions(-) create mode 100644 server/src/controllers/database-backup.controller.spec.ts create mode 100644 server/src/middleware/auth.guard.spec.ts diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index dbfd2fb112..c10a858ed9 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -45,7 +45,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N | `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices | | `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api | | `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/administration/system-integrity) | | server | api, microservices | -| `IMMICH_ALLOW_SETUP` | When `false` disables the `/auth/admin-sign-up` endpoint | `true` | server | api | +| `IMMICH_ALLOW_SETUP` | When `false` disables the `/auth/admin-sign-up` and `/admin/database-backups/start-restore` endpoints | `true` | server | api | \*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`. `TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution. diff --git a/e2e/src/responses.ts b/e2e/src/responses.ts index 5fd887c44b..1b3447767e 100644 --- a/e2e/src/responses.ts +++ b/e2e/src/responses.ts @@ -38,9 +38,6 @@ export const errorDto = { incorrectLogin: { message: 'Incorrect email or password', }, - alreadyHasAdmin: { - message: 'The server already has an admin', - }, }; export const signupResponseDto = { diff --git a/e2e/src/specs/maintenance/server/database-backups.e2e-spec.ts b/e2e/src/specs/maintenance/server/database-backups.e2e-spec.ts index cf6d752561..e757c721d6 100644 --- a/e2e/src/specs/maintenance/server/database-backups.e2e-spec.ts +++ b/e2e/src/specs/maintenance/server/database-backups.e2e-spec.ts @@ -108,7 +108,7 @@ describe('/admin/database-backups', () => { const { status, body } = await request(app).post('/admin/database-backups/start-restore').send(); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest('The server already has an admin')); + expect(body).toEqual(errorDto.badRequest('Admin setup is not available')); }); it.sequential('should enter maintenance mode in "database restore mode"', async () => { diff --git a/server/src/controllers/app.controller.ts b/server/src/controllers/app.controller.ts index 3fe9b49368..eca7e5bcb5 100644 --- a/server/src/controllers/app.controller.ts +++ b/server/src/controllers/app.controller.ts @@ -1,5 +1,6 @@ import { Controller, Get, Header } from '@nestjs/common'; import { ApiExcludeEndpoint } from '@nestjs/swagger'; +import { Authenticated } from 'src/middleware/auth.guard'; import { SystemConfigService } from 'src/services/system-config.service'; @Controller() @@ -8,6 +9,7 @@ export class AppController { @ApiExcludeEndpoint() @Get('.well-known/immich') + @Authenticated({ public: true }) getImmichWellKnown() { return { api: { @@ -18,6 +20,7 @@ export class AppController { @ApiExcludeEndpoint() @Get('custom.css') + @Authenticated({ public: true }) @Header('Content-Type', 'text/css') getCustomCss() { return this.service.getCustomCss(); diff --git a/server/src/controllers/auth.controller.spec.ts b/server/src/controllers/auth.controller.spec.ts index d105dd90b9..3bf1d59f8b 100644 --- a/server/src/controllers/auth.controller.spec.ts +++ b/server/src/controllers/auth.controller.spec.ts @@ -1,3 +1,4 @@ +import { BadRequestException } from '@nestjs/common'; import { AuthController } from 'src/controllers/auth.controller'; import { LoginResponseDto } from 'src/dtos/auth.dto'; import { AuthService } from 'src/services/auth.service'; @@ -76,6 +77,18 @@ describe(AuthController.name, () => { .send({ name: 'admin', password: 'password', email: 'admin@local' }); expect(status).toEqual(201); }); + + it('should not sign up an admin when setup is unavailable', async () => { + ctx.requireSetupAvailable.mockRejectedValue(new BadRequestException('Admin setup is not available')); + + const { status, body } = await request(ctx.getHttpServer()) + .post('/auth/admin-sign-up') + .send({ name, email, password }); + + expect(status).toEqual(400); + expect(body).toEqual(errorDto.badRequest('Admin setup is not available')); + expect(service.adminSignUp).not.toHaveBeenCalled(); + }); }); describe('POST /auth/login', () => { diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts index 63cdce4f32..b96ba4dee3 100644 --- a/server/src/controllers/auth.controller.ts +++ b/server/src/controllers/auth.controller.ts @@ -33,6 +33,7 @@ export class AuthController { description: 'Login with username and password and receive a session token.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) + @Authenticated({ public: true }) async login( @Res({ passthrough: true }) res: Response, @Body() loginCredential: LoginCredentialDto, @@ -55,6 +56,7 @@ export class AuthController { description: 'Create the first admin user in the system.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) + @Authenticated({ public: true, setup: true }) signUpAdmin(@Body() dto: SignUpDto): Promise { return this.service.adminSignUp(dto); } diff --git a/server/src/controllers/database-backup.controller.spec.ts b/server/src/controllers/database-backup.controller.spec.ts new file mode 100644 index 0000000000..43fa779e8d --- /dev/null +++ b/server/src/controllers/database-backup.controller.spec.ts @@ -0,0 +1,55 @@ +import { BadRequestException } from '@nestjs/common'; +import { DatabaseBackupController } from 'src/controllers/database-backup.controller'; +import { DatabaseBackupService } from 'src/services/database-backup.service'; +import { MaintenanceService } from 'src/services/maintenance.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +describe(DatabaseBackupController.name, () => { + let ctx: ControllerContext; + const service = automock(DatabaseBackupService, { args: [{ setContext: () => {} }], strict: false }); + const maintenanceService = mockBaseService(MaintenanceService); + + beforeAll(async () => { + ctx = await controllerSetup(DatabaseBackupController, [ + { provide: DatabaseBackupService, useValue: service }, + { provide: MaintenanceService, useValue: maintenanceService }, + ]); + return () => ctx.close(); + }); + + beforeEach(() => { + service.resetAllMocks(); + maintenanceService.resetAllMocks(); + ctx.reset(); + }); + + describe('GET /admin/database-backups', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get('/admin/database-backups').send(); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + }); + + describe('POST /admin/database-backups/start-restore', () => { + it('should not be an authenticated route', async () => { + maintenanceService.startRestoreFlow.mockResolvedValue({ jwt: 'jwt' }); + + await request(ctx.getHttpServer()).post('/admin/database-backups/start-restore').send(); + + expect(ctx.authenticate).not.toHaveBeenCalled(); + expect(ctx.requireSetupAvailable).toHaveBeenCalled(); + }); + + it('should not start a restore when setup is unavailable', async () => { + ctx.requireSetupAvailable.mockRejectedValue(new BadRequestException('Admin setup is not available')); + + const { status, body } = await request(ctx.getHttpServer()).post('/admin/database-backups/start-restore').send(); + + expect(status).toEqual(400); + expect(body).toEqual(errorDto.badRequest('Admin setup is not available')); + expect(maintenanceService.startRestoreFlow).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/server/src/controllers/database-backup.controller.ts b/server/src/controllers/database-backup.controller.ts index 737c8f3958..4cb2092d1b 100644 --- a/server/src/controllers/database-backup.controller.ts +++ b/server/src/controllers/database-backup.controller.ts @@ -71,6 +71,7 @@ export class DatabaseBackupController { description: 'Put Immich into maintenance mode to restore a backup (Immich must not be configured)', history: new HistoryBuilder().added('v2.5.0').alpha('v2.5.0'), }) + @Authenticated({ public: true, setup: true }) async startDatabaseRestoreFlow( @GetLoginDetails() loginDetails: LoginDetails, @Res({ passthrough: true }) res: Response, diff --git a/server/src/controllers/index.spec.ts b/server/src/controllers/index.spec.ts index 67962e8b3c..3d39a4dd3d 100644 --- a/server/src/controllers/index.spec.ts +++ b/server/src/controllers/index.spec.ts @@ -53,12 +53,10 @@ describe('controllers', () => { expect(new Set(reachableByNonAdmins)).toEqual(UNAUTHENTICATED_ADMIN_ROUTES); }); - it('should not authenticate the bootstrap routes under admin/', () => { - const authenticated = routes - .filter((route) => UNAUTHENTICATED_ADMIN_ROUTES.has(route.id) && route.auth !== undefined) - .map((route) => route.label); + it('should declare authentication on every route', () => { + const undeclared = routes.filter((route) => route.auth === undefined).map((route) => route.label); - expect(authenticated).toEqual([]); + expect(undeclared).toEqual([]); }); it('should require admin access for routes with an admin permission', () => { diff --git a/server/src/controllers/maintenance.controller.ts b/server/src/controllers/maintenance.controller.ts index d5f13e341c..3d8c6c1194 100644 --- a/server/src/controllers/maintenance.controller.ts +++ b/server/src/controllers/maintenance.controller.ts @@ -27,6 +27,7 @@ export class MaintenanceController { description: 'Fetch information about the currently running maintenance action.', history: new HistoryBuilder().added('v2.5.0').alpha('v2.5.0'), }) + @Authenticated({ public: true }) getMaintenanceStatus(): MaintenanceStatusResponseDto { return this.service.getMaintenanceStatus(); } @@ -48,6 +49,7 @@ export class MaintenanceController { description: 'Login with maintenance token or cookie to receive current information and perform further actions.', history: new HistoryBuilder().added('v2.3.0').alpha('v2.3.0'), }) + @Authenticated({ public: true }) maintenanceLogin(@Body() _dto: MaintenanceLoginDto): MaintenanceAuthDto { throw new BadRequestException('Not in maintenance mode'); } diff --git a/server/src/controllers/oauth.controller.ts b/server/src/controllers/oauth.controller.ts index 7f2313a058..54f5c1f10b 100644 --- a/server/src/controllers/oauth.controller.ts +++ b/server/src/controllers/oauth.controller.ts @@ -22,6 +22,7 @@ export class OAuthController { constructor(private service: AuthService) {} @Get('mobile-redirect') + @Authenticated({ public: true }) @Redirect() @Endpoint({ summary: 'Redirect OAuth to mobile', @@ -37,6 +38,7 @@ export class OAuthController { } @Post('authorize') + @Authenticated({ public: true }) @Endpoint({ summary: 'Start OAuth', description: 'Initiate the OAuth authorization process.', @@ -62,6 +64,7 @@ export class OAuthController { } @Post('callback') + @Authenticated({ public: true }) @Endpoint({ summary: 'Finish OAuth', description: 'Complete the OAuth authorization process by exchanging the authorization code for a session token.', @@ -115,6 +118,7 @@ export class OAuthController { } @Post('backchannel-logout') + @Authenticated({ public: true }) @HttpCode(HttpStatus.OK) @ApiConsumes('application/x-www-form-urlencoded') @Endpoint({ diff --git a/server/src/controllers/server.controller.ts b/server/src/controllers/server.controller.ts index f5ce4b851c..6407155492 100644 --- a/server/src/controllers/server.controller.ts +++ b/server/src/controllers/server.controller.ts @@ -64,6 +64,7 @@ export class ServerController { } @Get('ping') + @Authenticated({ public: true }) @Endpoint({ summary: 'Ping', description: 'Pong', @@ -74,6 +75,7 @@ export class ServerController { } @Get('version') + @Authenticated({ public: true }) @Endpoint({ summary: 'Get server version', description: 'Retrieve the current server version in semantic versioning (semver) format.', @@ -84,6 +86,7 @@ export class ServerController { } @Get('version-history') + @Authenticated({ public: true }) @Endpoint({ summary: 'Get version history', description: 'Retrieve a list of past versions the server has been on.', @@ -94,6 +97,7 @@ export class ServerController { } @Get('features') + @Authenticated({ public: true }) @Endpoint({ summary: 'Get features', description: 'Retrieve available features supported by this server.', @@ -104,6 +108,7 @@ export class ServerController { } @Get('config') + @Authenticated({ public: true }) @Endpoint({ summary: 'Get config', description: 'Retrieve the current server configuration.', @@ -125,6 +130,7 @@ export class ServerController { } @Get('media-types') + @Authenticated({ public: true }) @Endpoint({ summary: 'Get supported media types', description: 'Retrieve all media types supported by the server.', diff --git a/server/src/middleware/auth.guard.spec.ts b/server/src/middleware/auth.guard.spec.ts new file mode 100644 index 0000000000..5b094c03c1 --- /dev/null +++ b/server/src/middleware/auth.guard.spec.ts @@ -0,0 +1,82 @@ +import { ExecutionContext } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { Authenticated, AuthGuard } from 'src/middleware/auth.guard'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { AuthService } from 'src/services/auth.service'; +import { mockEnvData } from 'test/repositories/config.repository.mock'; +import { newTestService, ServiceMocks } from 'test/utils'; + +class TestController { + @Authenticated({ public: true, setup: true }) + setupRoute() {} + + @Authenticated({ public: true }) + publicRoute() {} + + undecoratedRoute() {} +} + +const contextFor = (handler: () => void) => + ({ + getHandler: () => handler, + switchToHttp: () => ({ getRequest: () => ({ headers: {}, query: {}, path: '/' }) }), + }) as unknown as ExecutionContext; + +describe(AuthGuard.name, () => { + let sut: AuthGuard; + let authService: AuthService; + let mocks: ServiceMocks; + + beforeEach(() => { + ({ sut: authService, mocks } = newTestService(AuthService)); + sut = new AuthGuard(mocks.logger as unknown as LoggingRepository, new Reflector(), authService); + }); + + describe('setup routes', () => { + it('should allow access while the server is awaiting its first admin', async () => { + mocks.user.hasAdmin.mockResolvedValue(false); + const authenticate = vitest.spyOn(authService, 'authenticate'); + + await expect(sut.canActivate(contextFor(TestController.prototype.setupRoute))).resolves.toBe(true); + + expect(authenticate).not.toHaveBeenCalled(); + }); + + it('should reject when setup is disabled', async () => { + mocks.config.getEnv.mockReturnValue(mockEnvData({ setup: { allow: false } })); + mocks.user.hasAdmin.mockResolvedValue(false); + + await expect(sut.canActivate(contextFor(TestController.prototype.setupRoute))).rejects.toThrowError( + 'Admin setup is not available', + ); + }); + + it('should reject when the server already has an admin', async () => { + mocks.user.hasAdmin.mockResolvedValue(true); + + await expect(sut.canActivate(contextFor(TestController.prototype.setupRoute))).rejects.toThrowError( + 'Admin setup is not available', + ); + }); + }); + + describe('public routes', () => { + it('should not require setup availability', async () => { + mocks.user.hasAdmin.mockResolvedValue(true); + + await expect(sut.canActivate(contextFor(TestController.prototype.publicRoute))).resolves.toBe(true); + }); + }); + + describe('undecorated routes', () => { + it('should be rejected', async () => { + const authenticate = vitest.spyOn(authService, 'authenticate'); + + await expect(sut.canActivate(contextFor(TestController.prototype.undecoratedRoute))).rejects.toThrowError( + 'does not declare @Authenticated()', + ); + + expect(authenticate).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/server/src/middleware/auth.guard.ts b/server/src/middleware/auth.guard.ts index d9870ec7b9..93bcfe26e7 100644 --- a/server/src/middleware/auth.guard.ts +++ b/server/src/middleware/auth.guard.ts @@ -17,23 +17,26 @@ import { getUserAgentDetails } from 'src/utils/request'; type AdminRoute = { admin?: true }; type SharedLinkRoute = { sharedLink?: true }; -export type AuthenticatedOptions = { permission?: Permission | false } & (AdminRoute | SharedLinkRoute); +type AuthorizedRoute = { permission?: Permission | false; public?: never; setup?: never } & ( + AdminRoute | SharedLinkRoute +); +type PublicRoute = { public: true; setup?: true; permission?: never; admin?: never; sharedLink?: never }; +export type AuthenticatedOptions = AuthorizedRoute | PublicRoute; type ReflectorTarget = Parameters[1]; /** Resolves the `@Authenticated()` options of a route handler, with the defaults applied. */ export const getAuthenticatedOptions = (reflector: Reflector, target: ReflectorTarget) => { const options = reflector.getAllAndOverride(MetadataKey.AuthRoute, [target]); - return options && { sharedLink: false, admin: false, ...options }; + return options && { sharedLink: false, admin: false, public: false, setup: false, ...options }; }; export const Authenticated = (options: AuthenticatedOptions = {}): MethodDecorator => { - const decorators: MethodDecorator[] = [ - ApiBearerAuth(), - ApiCookieAuth(), - ApiSecurity(MetadataKey.ApiKeySecurity), - SetMetadata(MetadataKey.AuthRoute, options), - ]; + const decorators: MethodDecorator[] = [SetMetadata(MetadataKey.AuthRoute, options)]; + + if (!options.public) { + decorators.push(ApiBearerAuth(), ApiCookieAuth(), ApiSecurity(MetadataKey.ApiKeySecurity)); + } if ((options as AdminRoute).admin) { decorators.push(ApiExtension(ApiCustomExtension.AdminOnly, true)); @@ -96,6 +99,14 @@ export class AuthGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise { const options = getAuthenticatedOptions(this.reflector, context.getHandler()); if (!options) { + throw new Error(`Route ${context.getHandler().name} does not declare @Authenticated()`); + } + + if (options.setup) { + await this.authService.requireSetupAvailable(); + } + + if (options.public) { return true; } diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index dad13ef5f5..48dcf5a509 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -307,16 +307,7 @@ describe(AuthService.name, () => { describe('adminSignUp', () => { const dto: SignUpDto = { email: 'test@immich.com', password: 'password', name: 'immich admin' }; - it('should only allow one admin', async () => { - mocks.user.getAdmin.mockResolvedValue({} as UserAdmin); - - await expect(sut.adminSignUp(dto)).rejects.toBeInstanceOf(BadRequestException); - - expect(mocks.user.getAdmin).toHaveBeenCalled(); - }); - it('should sign up the admin', async () => { - mocks.user.getAdmin.mockResolvedValue(void 0); mocks.user.create.mockResolvedValue({ ...userStub.admin, ...dto, @@ -334,7 +325,6 @@ describe(AuthService.name, () => { name: 'immich admin', }); - expect(mocks.user.getAdmin).toHaveBeenCalled(); expect(mocks.user.create).toHaveBeenCalled(); }); }); diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index f3be40f7dd..59e20276af 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -197,16 +197,6 @@ export class AuthService extends BaseService { } async adminSignUp(dto: SignUpDto): Promise { - const { setup } = this.configRepository.getEnv(); - if (!setup.allow) { - throw new BadRequestException('Admin setup is disabled'); - } - - const adminUser = await this.userRepository.getAdmin(); - if (adminUser) { - throw new BadRequestException('The server already has an admin'); - } - const admin = await this.createUser({ isAdmin: true, email: dto.email, diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index 6d17410441..2195c66a22 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -280,6 +280,17 @@ export class BaseService { return checkAccess(this.accessRepository, request); } + async isSetupAvailable(): Promise { + const { setup } = this.configRepository.getEnv(); + return setup.allow && !(await this.userRepository.hasAdmin()); + } + + async requireSetupAvailable(): Promise { + if (!(await this.isSetupAvailable())) { + throw new BadRequestException('Admin setup is not available'); + } + } + async createUser(dto: Insertable & { email: string }): Promise { const exists = await this.userRepository.getByEmail(dto.email); if (exists) { diff --git a/server/src/services/maintenance.service.spec.ts b/server/src/services/maintenance.service.spec.ts index e598f1c71d..1eaefb689d 100644 --- a/server/src/services/maintenance.service.spec.ts +++ b/server/src/services/maintenance.service.spec.ts @@ -134,6 +134,22 @@ describe(MaintenanceService.name, () => { }); }); + describe('startRestoreFlow', () => { + it('should start maintenance mode and return a jwt', async () => { + mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: false }); + + await expect(sut.startRestoreFlow()).resolves.toMatchObject({ jwt: expect.any(String) }); + + expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.MaintenanceMode, { + isMaintenanceMode: true, + secret: expect.stringMatching(/^\w{128}$/), + action: { + action: MaintenanceAction.SelectDatabaseRestore, + }, + }); + }); + }); + describe('createLoginUrl', () => { it('should fail outside of maintenance mode without secret', async () => { mocks.systemMetadata.get.mockResolvedValue({ isMaintenanceMode: false }); diff --git a/server/src/services/maintenance.service.ts b/server/src/services/maintenance.service.ts index ca1e05d93f..bcd6f2e834 100644 --- a/server/src/services/maintenance.service.ts +++ b/server/src/services/maintenance.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { OnEvent } from 'src/decorators'; import { MaintenanceAuthDto, @@ -59,11 +59,6 @@ export class MaintenanceService extends BaseService { } async startRestoreFlow(): Promise<{ jwt: string }> { - const adminUser = await this.userRepository.getAdmin(); - if (adminUser) { - throw new BadRequestException('The server already has an admin'); - } - return this.startMaintenance( { action: MaintenanceAction.SelectDatabaseRestore, diff --git a/server/src/services/server.service.spec.ts b/server/src/services/server.service.spec.ts index e1575a496a..b4b1af35d6 100644 --- a/server/src/services/server.service.spec.ts +++ b/server/src/services/server.service.spec.ts @@ -1,5 +1,6 @@ import { SystemMetadataKey } from 'src/enum'; import { ServerService } from 'src/services/server.service'; +import { mockEnvData } from 'test/repositories/config.repository.mock'; import { newTestService, ServiceMocks } from 'test/utils'; describe(ServerService.name, () => { @@ -161,7 +162,7 @@ describe(ServerService.name, () => { oauthButtonText: 'Login with OAuth', trashDays: 30, userDeleteDelay: 7, - isInitialized: undefined, + isInitialized: false, isOnboarded: false, externalDomain: '', publicUsers: true, @@ -172,6 +173,19 @@ describe(ServerService.name, () => { }); expect(mocks.systemMetadata.get).toHaveBeenCalled(); }); + + it('should be initialized once an admin exists', async () => { + mocks.user.hasAdmin.mockResolvedValue(true); + + await expect(sut.getSystemConfig()).resolves.toMatchObject({ isInitialized: true }); + }); + + it('should be initialized when setup is disabled', async () => { + mocks.config.getEnv.mockReturnValue(mockEnvData({ setup: { allow: false } })); + mocks.user.hasAdmin.mockResolvedValue(false); + + await expect(sut.getSystemConfig()).resolves.toMatchObject({ isInitialized: true }); + }); }); describe('getStats', () => { diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index ad212292ba..57342f9509 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -111,9 +111,8 @@ export class ServerService extends BaseService { } async getSystemConfig(): Promise { - const { setup } = this.configRepository.getEnv(); const config = await this.getConfig({ withCache: false }); - const isInitialized = !setup.allow || (await this.userRepository.hasAdmin()); + const isInitialized = !(await this.isSetupAvailable()); const onboarding = await this.systemMetadataRepository.get(SystemMetadataKey.AdminOnboarding); return { diff --git a/server/test/medium/responses.ts b/server/test/medium/responses.ts index b416b3b904..9b75bc85ec 100644 --- a/server/test/medium/responses.ts +++ b/server/test/medium/responses.ts @@ -35,7 +35,4 @@ export const errorDto = { incorrectLogin: { message: 'Incorrect email or password', }, - alreadyHasAdmin: { - message: 'The server already has an admin', - }, }; diff --git a/server/test/medium/specs/services/auth.service.spec.ts b/server/test/medium/specs/services/auth.service.spec.ts index 1fc306f790..5e5c880955 100644 --- a/server/test/medium/specs/services/auth.service.spec.ts +++ b/server/test/medium/specs/services/auth.service.spec.ts @@ -57,16 +57,6 @@ describe(AuthService.name, () => { }), ); }); - - it('should not allow a second admin to sign up', async () => { - const { sut, ctx } = setup(); - await ctx.newUser({ isAdmin: true }); - const dto = { name: 'Admin', email: 'admin@immich.cloud', password: 'password' }; - - const response = sut.adminSignUp(dto); - await expect(response).rejects.toThrow(BadRequestException); - await expect(response).rejects.toThrow('The server already has an admin'); - }); }); describe('login', () => { diff --git a/server/test/utils.ts b/server/test/utils.ts index b633cbc4de..2f32f3b412 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -89,6 +89,7 @@ import { assert, Mock, Mocked, vitest } from 'vitest'; export type ControllerContext = { authenticate: Mock; + requireSetupAvailable: Mock; getHttpServer: () => any; reset: () => void; close: () => Promise; @@ -124,7 +125,7 @@ export const controllerSetup = async (controller: new (...args: any[]) => unknow { provide: APP_GUARD, useClass: AuthGuard }, { provide: LoggingRepository, useValue: LoggingRepository.create() }, { provide: ClsService, useValue: { getId: vi.fn() } }, - { provide: AuthService, useValue: { authenticate: vi.fn() } }, + { provide: AuthService, useValue: { authenticate: vi.fn(), requireSetupAvailable: vi.fn() } }, ...providers, ], }) @@ -137,13 +138,17 @@ export const controllerSetup = async (controller: new (...args: any[]) => unknow await app.init(); // allow the AuthController to override the AuthService itself - const authenticate = app.get>(AuthService).authenticate as Mock; + const resolvedAuthService = app.get>(AuthService); + const authenticate = resolvedAuthService.authenticate as Mock; + const requireSetupAvailable = resolvedAuthService.requireSetupAvailable as Mock; return { authenticate, + requireSetupAvailable, getHttpServer: () => app.getHttpServer(), reset: () => { authenticate.mockReset(); + requireSetupAvailable.mockReset(); }, close: async () => { await app.close(); @@ -184,10 +189,12 @@ export const automock = ( const mocks: Mock[] = []; const instance = new Dependency(...args); - const propertyNames = new Set([ - ...Object.getOwnPropertyNames(Dependency.prototype), - ...Object.getOwnPropertyNames(instance), - ]); + const propertyNames = new Set(Object.getOwnPropertyNames(instance)); + for (let proto = Dependency.prototype; proto && proto !== Object.prototype; proto = Object.getPrototypeOf(proto)) { + for (const property of Object.getOwnPropertyNames(proto)) { + propertyNames.add(property); + } + } for (const property of propertyNames) { if (property === 'constructor') { continue; From 6afcc39fb240be0596edf6a6f99826253132a018 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 30 Jul 2026 21:12:42 +0200 Subject: [PATCH 06/81] chore: sequence pnpm installs in mise tasks (#30412) --- e2e/mise.toml | 17 +++++++++-------- packages/cli/mise.toml | 6 ++++-- web/mise.toml | 11 ++++++++--- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/e2e/mise.toml b/e2e/mise.toml index b149922564..487f4ebf46 100644 --- a/e2e/mise.toml +++ b/e2e/mise.toml @@ -1,5 +1,5 @@ [tasks.install] -run = "pnpm install --filter immich-e2e --frozen-lockfile" +run = "pnpm install --filter immich-e2e... --frozen-lockfile" [tasks.build] dir = "{{ config_root }}" @@ -40,18 +40,19 @@ run = "tsc --noEmit" [tasks.ci-setup] -depends = [ - "//:sdk:install", - "//:sdk:build", - "//packages/cli:install", - "//packages/cli:build", +run = [ + { task = "//:sdk:install" }, + { task = "//:sdk:build" }, + { task = "//packages/cli:install" }, + { task = "//packages/cli:build" }, + { task = ":install" }, ] -run = { task = ":install" } [tasks.ci-unit] -depends = ["//:sdk:install", "//:sdk:build"] run = [ + { task = "//:sdk:install" }, + { task = "//:sdk:build" }, { task = ":install" }, { task = ":format" }, { task = ":lint" }, diff --git a/packages/cli/mise.toml b/packages/cli/mise.toml index 28d5e1858f..320ce001d8 100644 --- a/packages/cli/mise.toml +++ b/packages/cli/mise.toml @@ -29,16 +29,18 @@ env._.path = "./node_modules/.bin" run = "tsc --noEmit" [tasks.ci-publish] -depends = ["//:sdk:install", "//:sdk:build"] run = [ + { task = "//:sdk:install" }, + { task = "//:sdk:build" }, { task = ":install" }, { task = ":build" }, "pnpm publish --provenance --no-git-checks", ] [tasks.ci-unit] -depends = ["//:sdk:install", "//:sdk:build"] run = [ + { task = "//:sdk:install" }, + { task = "//:sdk:build" }, { task = ":install" }, { task = ":format" }, { task = ":lint" }, diff --git a/web/mise.toml b/web/mise.toml index b0d41317cb..7b9e3c2f3b 100644 --- a/web/mise.toml +++ b/web/mise.toml @@ -11,8 +11,12 @@ run = "pnpm run build:stats" run = "pnpm run preview" [tasks.start] -depends = [":install", "//:sdk:install", "//:sdk:build"] -run = "pnpm run dev" +run = [ + { task = ":install" }, + { task = "//:sdk:install" }, + { task = "//:sdk:build" }, + "pnpm run dev", +] [tasks."start-demo"] env.IMMICH_SERVER_URL = "https://demo.immich.app" @@ -43,8 +47,9 @@ run = "pnpm run check:svelte" run = { tasks = [":check-typescript", ":check-svelte"] } [tasks.ci-unit] -depends = ["//:sdk:install", "//:sdk:build"] run = [ + { task = "//:sdk:install" }, + { task = "//:sdk:build" }, { task = ":install" }, { task = ":format" }, { task = ":check" }, From 6e1e79585ecd17d3ae29693629998bce093be28d Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 30 Jul 2026 21:13:12 +0200 Subject: [PATCH 07/81] chore: install mise tools from the lockfile in the server image (#30416) --- server/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/Dockerfile b/server/Dockerfile index b7a4e105a3..df9d8ad7ba 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -60,12 +60,13 @@ COPY --from=ghcr.io/jdx/mise:2026.7.15@sha256:e62097049bfc980de5d9a25fbe16431e24 WORKDIR /app COPY ./mise.toml ./mise.toml +COPY ./mise.lock ./mise.lock COPY ./packages/plugin-core/mise.toml ./packages/plugin-core/ ENV MISE_TRUSTED_CONFIG_PATHS=/app/mise.toml ENV MISE_DATA_DIR=/buildcache/mise ENV MISE_DISABLE_TOOLS=flutter RUN --mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \ - mise install + mise install --locked COPY ./packages/sdk ./packages/sdk/ COPY ./packages/plugin-core ./packages/plugin-core/ From 7c44c29a8f52b9a42981579fad6e06b31f044fa2 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 30 Jul 2026 21:13:44 +0200 Subject: [PATCH 08/81] chore: deflake album to asset backfill sync test (#30417) --- server/test/medium/specs/sync/sync-album-to-asset.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/test/medium/specs/sync/sync-album-to-asset.spec.ts b/server/test/medium/specs/sync/sync-album-to-asset.spec.ts index a0802abe73..0fd58c527c 100644 --- a/server/test/medium/specs/sync/sync-album-to-asset.spec.ts +++ b/server/test/medium/specs/sync/sync-album-to-asset.spec.ts @@ -161,12 +161,14 @@ describe(SyncRequestType.AlbumToAssetsV1, () => { // backfill needs assets with an older updateId const { asset: sharedAsset1 } = await ctx.newAsset({ ownerId: user2.id }); + await wait(2); const { asset: sharedAsset2 } = await ctx.newAsset({ ownerId: user2.id }); await wait(2); const { album: sharedAlbum } = await ctx.newAlbum({ ownerId: user2.id }); await ctx.newAlbumAsset({ albumId: sharedAlbum.id, assetId: sharedAsset1.id }); + await wait(2); await ctx.newAlbumAsset({ albumId: sharedAlbum.id, assetId: sharedAsset2.id }); await wait(2); From 4a68a87531d62a3ac48631d48ee158e8d1eed080 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 30 Jul 2026 21:14:18 +0200 Subject: [PATCH 09/81] fix: use trixie-slim base image for cli and e2e-auth-server (#30411) The alpine suffix part wasn't getting updated by renovate, and the 3.20 chain stopped getting node updates too. This also just aligns the base image distro with what we use elsewere. --- packages/cli/Dockerfile | 2 +- packages/e2e-auth-server/Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/Dockerfile b/packages/cli/Dockerfile index ee2a4294bd..716522ecd7 100644 --- a/packages/cli/Dockerfile +++ b/packages/cli/Dockerfile @@ -1,4 +1,4 @@ -FROM node:24.1.0-alpine3.20@sha256:8fe019e0d57dbdce5f5c27c0b63d2775cf34b00e3755a7dea969802d7e0c2b25 AS core +FROM node:24.18.0-trixie-slim@sha256:ae91dcc111a68c9d2d81ff2a17bda61be126426176fde6fe7d08ab13b7f50573 AS core WORKDIR /usr/src/app COPY package* pnpm* .pnpmfile.cjs ./ diff --git a/packages/e2e-auth-server/Dockerfile b/packages/e2e-auth-server/Dockerfile index 3a49307316..bfc939c084 100644 --- a/packages/e2e-auth-server/Dockerfile +++ b/packages/e2e-auth-server/Dockerfile @@ -1,4 +1,4 @@ -FROM node:24.1.0-alpine3.20@sha256:8fe019e0d57dbdce5f5c27c0b63d2775cf34b00e3755a7dea969802d7e0c2b25 +FROM node:24.18.0-trixie-slim@sha256:ae91dcc111a68c9d2d81ff2a17bda61be126426176fde6fe7d08ab13b7f50573 WORKDIR /usr/src/app COPY package* pnpm* .pnpmfile.cjs ./ COPY ./packages ./packages/ From 9a143f0047173c45adb570dc1347921bc55dbbe4 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 30 Jul 2026 12:14:50 -0700 Subject: [PATCH 10/81] chore(mobile): remove Pigeon generated code (#30343) --- mobile/.gitignore | 5 + .../immich/background/BackgroundWorker.g.kt | 453 ---------- .../background/BackgroundWorkerLock.g.kt | 95 -- .../immich/connectivity/Connectivity.g.kt | 116 --- .../app/alextran/immich/core/Network.g.kt | 451 ---------- .../alextran/immich/images/LocalImages.g.kt | 140 --- .../alextran/immich/images/RemoteImages.g.kt | 123 --- .../immich/permission/PermissionApi.g.kt | 169 ---- .../app/alextran/immich/sync/Messages.g.kt | 823 ------------------ .../immich/viewintent/ViewIntent.g.kt | 292 ------- .../Background/BackgroundWorker.g.swift | 418 --------- .../Runner/Connectivity/Connectivity.g.swift | 129 --- mobile/ios/Runner/Core/Network.g.swift | 406 --------- mobile/ios/Runner/Images/LocalImages.g.swift | 139 --- mobile/ios/Runner/Images/RemoteImages.g.swift | 134 --- .../Runner/Permission/PermissionApi.g.swift | 168 ---- mobile/ios/Runner/Sync/Messages.g.swift | 777 ----------------- .../lib/platform/background_worker_api.g.dart | 365 -------- .../background_worker_lock_api.g.dart | 90 -- mobile/lib/platform/connectivity_api.g.dart | 89 -- mobile/lib/platform/local_image_api.g.dart | 128 --- mobile/lib/platform/native_sync_api.g.dart | 708 --------------- mobile/lib/platform/network_api.g.dart | 331 ------- mobile/lib/platform/permission_api.g.dart | 146 ---- mobile/lib/platform/remote_image_api.g.dart | 114 --- mobile/lib/platform/thumbnail_api.g.dart | 142 --- mobile/lib/platform/view_intent_api.g.dart | 191 ---- 27 files changed, 5 insertions(+), 7137 deletions(-) delete mode 100644 mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt delete mode 100644 mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerLock.g.kt delete mode 100644 mobile/android/app/src/main/kotlin/app/alextran/immich/connectivity/Connectivity.g.kt delete mode 100644 mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt delete mode 100644 mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImages.g.kt delete mode 100644 mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImages.g.kt delete mode 100644 mobile/android/app/src/main/kotlin/app/alextran/immich/permission/PermissionApi.g.kt delete mode 100644 mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt delete mode 100644 mobile/android/app/src/main/kotlin/app/alextran/immich/viewintent/ViewIntent.g.kt delete mode 100644 mobile/ios/Runner/Background/BackgroundWorker.g.swift delete mode 100644 mobile/ios/Runner/Connectivity/Connectivity.g.swift delete mode 100644 mobile/ios/Runner/Core/Network.g.swift delete mode 100644 mobile/ios/Runner/Images/LocalImages.g.swift delete mode 100644 mobile/ios/Runner/Images/RemoteImages.g.swift delete mode 100644 mobile/ios/Runner/Permission/PermissionApi.g.swift delete mode 100644 mobile/ios/Runner/Sync/Messages.g.swift delete mode 100644 mobile/lib/platform/background_worker_api.g.dart delete mode 100644 mobile/lib/platform/background_worker_lock_api.g.dart delete mode 100644 mobile/lib/platform/connectivity_api.g.dart delete mode 100644 mobile/lib/platform/local_image_api.g.dart delete mode 100644 mobile/lib/platform/native_sync_api.g.dart delete mode 100644 mobile/lib/platform/network_api.g.dart delete mode 100644 mobile/lib/platform/permission_api.g.dart delete mode 100644 mobile/lib/platform/remote_image_api.g.dart delete mode 100644 mobile/lib/platform/thumbnail_api.g.dart delete mode 100644 mobile/lib/platform/view_intent_api.g.dart diff --git a/mobile/.gitignore b/mobile/.gitignore index 64aa4a8dd7..bdddf7bbcc 100644 --- a/mobile/.gitignore +++ b/mobile/.gitignore @@ -34,6 +34,11 @@ lib/**/*.drift.dart test/drift/main/generated/ +# Pigeon related +/lib/platform/*.g.dart +/ios/**/*.g.swift +/android/**/*.g.kt + # Web related lib/generated_plugin_registrant.dart diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt deleted file mode 100644 index 3fcaed34bc..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt +++ /dev/null @@ -1,453 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") - -package app.alextran.immich.background - -import android.util.Log -import io.flutter.plugin.common.BasicMessageChannel -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MessageCodec -import io.flutter.plugin.common.StandardMethodCodec -import io.flutter.plugin.common.StandardMessageCodec -import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer -private object BackgroundWorkerPigeonUtils { - - fun createConnectionError(channelName: String): FlutterError { - return FlutterError("channel-error", "Unable to establish connection on channel: '$channelName'.", "") } - - fun wrapResult(result: Any?): List { - return listOf(result) - } - - fun wrapError(exception: Throwable): List { - return if (exception is FlutterError) { - listOf( - exception.code, - exception.message, - exception.details - ) - } else { - listOf( - exception.javaClass.simpleName, - exception.toString(), - "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) - ) - } - } - fun doubleEquals(a: Double, b: Double): Boolean { - // Normalize -0.0 to 0.0 and handle NaN equality. - return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN()) - } - - fun floatEquals(a: Float, b: Float): Boolean { - // Normalize -0.0 to 0.0 and handle NaN equality. - return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN()) - } - - fun doubleHash(d: Double): Int { - // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. - val normalized = if (d == 0.0) 0.0 else d - val bits = java.lang.Double.doubleToLongBits(normalized) - return (bits xor (bits ushr 32)).toInt() - } - - fun floatHash(f: Float): Int { - // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. - val normalized = if (f == 0.0f) 0.0f else f - return java.lang.Float.floatToIntBits(normalized) - } - - fun deepEquals(a: Any?, b: Any?): Boolean { - if (a === b) { - return true - } - if (a == null || b == null) { - return false - } - if (a is ByteArray && b is ByteArray) { - return a.contentEquals(b) - } - if (a is IntArray && b is IntArray) { - return a.contentEquals(b) - } - if (a is LongArray && b is LongArray) { - return a.contentEquals(b) - } - if (a is DoubleArray && b is DoubleArray) { - if (a.size != b.size) return false - for (i in a.indices) { - if (!doubleEquals(a[i], b[i])) return false - } - return true - } - if (a is FloatArray && b is FloatArray) { - if (a.size != b.size) return false - for (i in a.indices) { - if (!floatEquals(a[i], b[i])) return false - } - return true - } - if (a is Array<*> && b is Array<*>) { - if (a.size != b.size) return false - for (i in a.indices) { - if (!deepEquals(a[i], b[i])) return false - } - return true - } - if (a is List<*> && b is List<*>) { - if (a.size != b.size) return false - val iterA = a.iterator() - val iterB = b.iterator() - while (iterA.hasNext() && iterB.hasNext()) { - if (!deepEquals(iterA.next(), iterB.next())) return false - } - return true - } - if (a is Map<*, *> && b is Map<*, *>) { - if (a.size != b.size) return false - for (entry in a) { - val key = entry.key - var found = false - for (bEntry in b) { - if (deepEquals(key, bEntry.key)) { - if (deepEquals(entry.value, bEntry.value)) { - found = true - break - } else { - return false - } - } - } - if (!found) return false - } - return true - } - if (a is Double && b is Double) { - return doubleEquals(a, b) - } - if (a is Float && b is Float) { - return floatEquals(a, b) - } - return a == b - } - - fun deepHash(value: Any?): Int { - return when (value) { - null -> 0 - is ByteArray -> value.contentHashCode() - is IntArray -> value.contentHashCode() - is LongArray -> value.contentHashCode() - is DoubleArray -> { - var result = 1 - for (item in value) { - result = 31 * result + doubleHash(item) - } - result - } - is FloatArray -> { - var result = 1 - for (item in value) { - result = 31 * result + floatHash(item) - } - result - } - is Array<*> -> { - var result = 1 - for (item in value) { - result = 31 * result + deepHash(item) - } - result - } - is List<*> -> { - var result = 1 - for (item in value) { - result = 31 * result + deepHash(item) - } - result - } - is Map<*, *> -> { - var result = 0 - for (entry in value) { - result += ((deepHash(entry.key) * 31) xor deepHash(entry.value)) - } - result - } - is Double -> doubleHash(value) - is Float -> floatHash(value) - else -> value.hashCode() - } - } - -} - -/** - * Error class for passing custom error details to Flutter via a thrown PlatformException. - * @property code The error code. - * @property message The error message. - * @property details The error details. Must be a datatype supported by the api codec. - */ -class FlutterError ( - val code: String, - override val message: String? = null, - val details: Any? = null -) : RuntimeException() - -/** Generated class from Pigeon that represents data sent in messages. */ -data class BackgroundWorkerSettings ( - val requiresCharging: Boolean, - val minimumDelaySeconds: Long -) - { - companion object { - fun fromList(pigeonVar_list: List): BackgroundWorkerSettings { - val requiresCharging = pigeonVar_list[0] as Boolean - val minimumDelaySeconds = pigeonVar_list[1] as Long - return BackgroundWorkerSettings(requiresCharging, minimumDelaySeconds) - } - } - fun toList(): List { - return listOf( - requiresCharging, - minimumDelaySeconds, - ) - } - override fun equals(other: Any?): Boolean { - if (other == null || other.javaClass != javaClass) { - return false - } - if (this === other) { - return true - } - val other = other as BackgroundWorkerSettings - return BackgroundWorkerPigeonUtils.deepEquals(this.requiresCharging, other.requiresCharging) && BackgroundWorkerPigeonUtils.deepEquals(this.minimumDelaySeconds, other.minimumDelaySeconds) - } - - override fun hashCode(): Int { - var result = javaClass.hashCode() - result = 31 * result + BackgroundWorkerPigeonUtils.deepHash(this.requiresCharging) - result = 31 * result + BackgroundWorkerPigeonUtils.deepHash(this.minimumDelaySeconds) - return result - } -} -private open class BackgroundWorkerPigeonCodec : StandardMessageCodec() { - override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { - return when (type) { - 129.toByte() -> { - return (readValue(buffer) as? List)?.let { - BackgroundWorkerSettings.fromList(it) - } - } - else -> super.readValueOfType(type, buffer) - } - } - override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { - when (value) { - is BackgroundWorkerSettings -> { - stream.write(129) - writeValue(stream, value.toList()) - } - else -> super.writeValue(stream, value) - } - } -} - -/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ -interface BackgroundWorkerFgHostApi { - fun enable() - fun saveNotificationMessage(title: String, body: String) - fun configure(settings: BackgroundWorkerSettings) - fun disable() - - companion object { - /** The codec used by BackgroundWorkerFgHostApi. */ - val codec: MessageCodec by lazy { - BackgroundWorkerPigeonCodec() - } - /** Sets up an instance of `BackgroundWorkerFgHostApi` to handle messages through the `binaryMessenger`. */ - @JvmOverloads - fun setUp(binaryMessenger: BinaryMessenger, api: BackgroundWorkerFgHostApi?, messageChannelSuffix: String = "") { - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.enable() - listOf(null) - } catch (exception: Throwable) { - BackgroundWorkerPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.saveNotificationMessage$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val titleArg = args[0] as String - val bodyArg = args[1] as String - val wrapped: List = try { - api.saveNotificationMessage(titleArg, bodyArg) - listOf(null) - } catch (exception: Throwable) { - BackgroundWorkerPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.configure$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val settingsArg = args[0] as BackgroundWorkerSettings - val wrapped: List = try { - api.configure(settingsArg) - listOf(null) - } catch (exception: Throwable) { - BackgroundWorkerPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.disable() - listOf(null) - } catch (exception: Throwable) { - BackgroundWorkerPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - } - } -} -/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ -interface BackgroundWorkerBgHostApi { - fun onInitialized() - fun close() - - companion object { - /** The codec used by BackgroundWorkerBgHostApi. */ - val codec: MessageCodec by lazy { - BackgroundWorkerPigeonCodec() - } - /** Sets up an instance of `BackgroundWorkerBgHostApi` to handle messages through the `binaryMessenger`. */ - @JvmOverloads - fun setUp(binaryMessenger: BinaryMessenger, api: BackgroundWorkerBgHostApi?, messageChannelSuffix: String = "") { - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.onInitialized$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.onInitialized() - listOf(null) - } catch (exception: Throwable) { - BackgroundWorkerPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.close() - listOf(null) - } catch (exception: Throwable) { - BackgroundWorkerPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - } - } -} -/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */ -class BackgroundWorkerFlutterApi(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") { - companion object { - /** The codec used by BackgroundWorkerFlutterApi. */ - val codec: MessageCodec by lazy { - BackgroundWorkerPigeonCodec() - } - } - fun onIosUpload(isRefreshArg: Boolean, maxSecondsArg: Long?, callback: (Result) -> Unit) -{ - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(isRefreshArg, maxSecondsArg)) { - if (it is List<*>) { - if (it.size > 1) { - callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) - } else { - callback(Result.success(Unit)) - } - } else { - callback(Result.failure(BackgroundWorkerPigeonUtils.createConnectionError(channelName))) - } - } - } - fun onAndroidUpload(maxMinutesArg: Long?, callback: (Result) -> Unit) -{ - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(maxMinutesArg)) { - if (it is List<*>) { - if (it.size > 1) { - callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) - } else { - callback(Result.success(Unit)) - } - } else { - callback(Result.failure(BackgroundWorkerPigeonUtils.createConnectionError(channelName))) - } - } - } - fun cancel(callback: (Result) -> Unit) -{ - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.cancel$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(null) { - if (it is List<*>) { - if (it.size > 1) { - callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) - } else { - callback(Result.success(Unit)) - } - } else { - callback(Result.failure(BackgroundWorkerPigeonUtils.createConnectionError(channelName))) - } - } - } -} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerLock.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerLock.g.kt deleted file mode 100644 index 4e2e382c2b..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerLock.g.kt +++ /dev/null @@ -1,95 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") - -package app.alextran.immich.background - -import android.util.Log -import io.flutter.plugin.common.BasicMessageChannel -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MessageCodec -import io.flutter.plugin.common.StandardMethodCodec -import io.flutter.plugin.common.StandardMessageCodec -import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer -private object BackgroundWorkerLockPigeonUtils { - - fun wrapResult(result: Any?): List { - return listOf(result) - } - - fun wrapError(exception: Throwable): List { - return if (exception is FlutterError) { - listOf( - exception.code, - exception.message, - exception.details - ) - } else { - listOf( - exception.javaClass.simpleName, - exception.toString(), - "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) - ) - } - } -} -private open class BackgroundWorkerLockPigeonCodec : StandardMessageCodec() { - override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { - return super.readValueOfType(type, buffer) - } - override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { - super.writeValue(stream, value) - } -} - -/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ -interface BackgroundWorkerLockApi { - fun lock() - fun unlock() - - companion object { - /** The codec used by BackgroundWorkerLockApi. */ - val codec: MessageCodec by lazy { - BackgroundWorkerLockPigeonCodec() - } - /** Sets up an instance of `BackgroundWorkerLockApi` to handle messages through the `binaryMessenger`. */ - @JvmOverloads - fun setUp(binaryMessenger: BinaryMessenger, api: BackgroundWorkerLockApi?, messageChannelSuffix: String = "") { - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.lock$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.lock() - listOf(null) - } catch (exception: Throwable) { - BackgroundWorkerLockPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.unlock$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.unlock() - listOf(null) - } catch (exception: Throwable) { - BackgroundWorkerLockPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - } - } -} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/connectivity/Connectivity.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/connectivity/Connectivity.g.kt deleted file mode 100644 index aec1f06164..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/connectivity/Connectivity.g.kt +++ /dev/null @@ -1,116 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") - -package app.alextran.immich.connectivity - -import android.util.Log -import io.flutter.plugin.common.BasicMessageChannel -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MessageCodec -import io.flutter.plugin.common.StandardMethodCodec -import io.flutter.plugin.common.StandardMessageCodec -import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer -private object ConnectivityPigeonUtils { - - fun wrapResult(result: Any?): List { - return listOf(result) - } - - fun wrapError(exception: Throwable): List { - return if (exception is FlutterError) { - listOf( - exception.code, - exception.message, - exception.details - ) - } else { - listOf( - exception.javaClass.simpleName, - exception.toString(), - "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) - ) - } - } -} - -/** - * Error class for passing custom error details to Flutter via a thrown PlatformException. - * @property code The error code. - * @property message The error message. - * @property details The error details. Must be a datatype supported by the api codec. - */ -class FlutterError ( - val code: String, - override val message: String? = null, - val details: Any? = null -) : RuntimeException() - -enum class NetworkCapability(val raw: Int) { - CELLULAR(0), - WIFI(1), - VPN(2), - UNMETERED(3); - - companion object { - fun ofRaw(raw: Int): NetworkCapability? { - return values().firstOrNull { it.raw == raw } - } - } -} -private open class ConnectivityPigeonCodec : StandardMessageCodec() { - override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { - return when (type) { - 129.toByte() -> { - return (readValue(buffer) as Long?)?.let { - NetworkCapability.ofRaw(it.toInt()) - } - } - else -> super.readValueOfType(type, buffer) - } - } - override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { - when (value) { - is NetworkCapability -> { - stream.write(129) - writeValue(stream, value.raw.toLong()) - } - else -> super.writeValue(stream, value) - } - } -} - -/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ -interface ConnectivityApi { - fun getCapabilities(): List - - companion object { - /** The codec used by ConnectivityApi. */ - val codec: MessageCodec by lazy { - ConnectivityPigeonCodec() - } - /** Sets up an instance of `ConnectivityApi` to handle messages through the `binaryMessenger`. */ - @JvmOverloads - fun setUp(binaryMessenger: BinaryMessenger, api: ConnectivityApi?, messageChannelSuffix: String = "") { - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val taskQueue = binaryMessenger.makeBackgroundTaskQueue() - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities$separatedMessageChannelSuffix", codec, taskQueue) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - listOf(api.getCapabilities()) - } catch (exception: Throwable) { - ConnectivityPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - } - } -} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt deleted file mode 100644 index c380a0a6a5..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/core/Network.g.kt +++ /dev/null @@ -1,451 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") - -package app.alextran.immich.core - -import android.util.Log -import io.flutter.plugin.common.BasicMessageChannel -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MessageCodec -import io.flutter.plugin.common.StandardMethodCodec -import io.flutter.plugin.common.StandardMessageCodec -import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer -private object NetworkPigeonUtils { - - fun wrapResult(result: Any?): List { - return listOf(result) - } - - fun wrapError(exception: Throwable): List { - return if (exception is FlutterError) { - listOf( - exception.code, - exception.message, - exception.details - ) - } else { - listOf( - exception.javaClass.simpleName, - exception.toString(), - "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) - ) - } - } - fun doubleEquals(a: Double, b: Double): Boolean { - // Normalize -0.0 to 0.0 and handle NaN equality. - return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN()) - } - - fun floatEquals(a: Float, b: Float): Boolean { - // Normalize -0.0 to 0.0 and handle NaN equality. - return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN()) - } - - fun doubleHash(d: Double): Int { - // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. - val normalized = if (d == 0.0) 0.0 else d - val bits = java.lang.Double.doubleToLongBits(normalized) - return (bits xor (bits ushr 32)).toInt() - } - - fun floatHash(f: Float): Int { - // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. - val normalized = if (f == 0.0f) 0.0f else f - return java.lang.Float.floatToIntBits(normalized) - } - - fun deepEquals(a: Any?, b: Any?): Boolean { - if (a === b) { - return true - } - if (a == null || b == null) { - return false - } - if (a is ByteArray && b is ByteArray) { - return a.contentEquals(b) - } - if (a is IntArray && b is IntArray) { - return a.contentEquals(b) - } - if (a is LongArray && b is LongArray) { - return a.contentEquals(b) - } - if (a is DoubleArray && b is DoubleArray) { - if (a.size != b.size) return false - for (i in a.indices) { - if (!doubleEquals(a[i], b[i])) return false - } - return true - } - if (a is FloatArray && b is FloatArray) { - if (a.size != b.size) return false - for (i in a.indices) { - if (!floatEquals(a[i], b[i])) return false - } - return true - } - if (a is Array<*> && b is Array<*>) { - if (a.size != b.size) return false - for (i in a.indices) { - if (!deepEquals(a[i], b[i])) return false - } - return true - } - if (a is List<*> && b is List<*>) { - if (a.size != b.size) return false - val iterA = a.iterator() - val iterB = b.iterator() - while (iterA.hasNext() && iterB.hasNext()) { - if (!deepEquals(iterA.next(), iterB.next())) return false - } - return true - } - if (a is Map<*, *> && b is Map<*, *>) { - if (a.size != b.size) return false - for (entry in a) { - val key = entry.key - var found = false - for (bEntry in b) { - if (deepEquals(key, bEntry.key)) { - if (deepEquals(entry.value, bEntry.value)) { - found = true - break - } else { - return false - } - } - } - if (!found) return false - } - return true - } - if (a is Double && b is Double) { - return doubleEquals(a, b) - } - if (a is Float && b is Float) { - return floatEquals(a, b) - } - return a == b - } - - fun deepHash(value: Any?): Int { - return when (value) { - null -> 0 - is ByteArray -> value.contentHashCode() - is IntArray -> value.contentHashCode() - is LongArray -> value.contentHashCode() - is DoubleArray -> { - var result = 1 - for (item in value) { - result = 31 * result + doubleHash(item) - } - result - } - is FloatArray -> { - var result = 1 - for (item in value) { - result = 31 * result + floatHash(item) - } - result - } - is Array<*> -> { - var result = 1 - for (item in value) { - result = 31 * result + deepHash(item) - } - result - } - is List<*> -> { - var result = 1 - for (item in value) { - result = 31 * result + deepHash(item) - } - result - } - is Map<*, *> -> { - var result = 0 - for (entry in value) { - result += ((deepHash(entry.key) * 31) xor deepHash(entry.value)) - } - result - } - is Double -> doubleHash(value) - is Float -> floatHash(value) - else -> value.hashCode() - } - } - -} - -/** - * Error class for passing custom error details to Flutter via a thrown PlatformException. - * @property code The error code. - * @property message The error message. - * @property details The error details. Must be a datatype supported by the api codec. - */ -class FlutterError ( - val code: String, - override val message: String? = null, - val details: Any? = null -) : RuntimeException() - -/** Generated class from Pigeon that represents data sent in messages. */ -data class ClientCertData ( - val data: ByteArray, - val password: String -) - { - companion object { - fun fromList(pigeonVar_list: List): ClientCertData { - val data = pigeonVar_list[0] as ByteArray - val password = pigeonVar_list[1] as String - return ClientCertData(data, password) - } - } - fun toList(): List { - return listOf( - data, - password, - ) - } - override fun equals(other: Any?): Boolean { - if (other == null || other.javaClass != javaClass) { - return false - } - if (this === other) { - return true - } - val other = other as ClientCertData - return NetworkPigeonUtils.deepEquals(this.data, other.data) && NetworkPigeonUtils.deepEquals(this.password, other.password) - } - - override fun hashCode(): Int { - var result = javaClass.hashCode() - result = 31 * result + NetworkPigeonUtils.deepHash(this.data) - result = 31 * result + NetworkPigeonUtils.deepHash(this.password) - return result - } -} - -/** Generated class from Pigeon that represents data sent in messages. */ -data class ClientCertPrompt ( - val title: String, - val message: String, - val cancel: String, - val confirm: String -) - { - companion object { - fun fromList(pigeonVar_list: List): ClientCertPrompt { - val title = pigeonVar_list[0] as String - val message = pigeonVar_list[1] as String - val cancel = pigeonVar_list[2] as String - val confirm = pigeonVar_list[3] as String - return ClientCertPrompt(title, message, cancel, confirm) - } - } - fun toList(): List { - return listOf( - title, - message, - cancel, - confirm, - ) - } - override fun equals(other: Any?): Boolean { - if (other == null || other.javaClass != javaClass) { - return false - } - if (this === other) { - return true - } - val other = other as ClientCertPrompt - return NetworkPigeonUtils.deepEquals(this.title, other.title) && NetworkPigeonUtils.deepEquals(this.message, other.message) && NetworkPigeonUtils.deepEquals(this.cancel, other.cancel) && NetworkPigeonUtils.deepEquals(this.confirm, other.confirm) - } - - override fun hashCode(): Int { - var result = javaClass.hashCode() - result = 31 * result + NetworkPigeonUtils.deepHash(this.title) - result = 31 * result + NetworkPigeonUtils.deepHash(this.message) - result = 31 * result + NetworkPigeonUtils.deepHash(this.cancel) - result = 31 * result + NetworkPigeonUtils.deepHash(this.confirm) - return result - } -} -private open class NetworkPigeonCodec : StandardMessageCodec() { - override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { - return when (type) { - 129.toByte() -> { - return (readValue(buffer) as? List)?.let { - ClientCertData.fromList(it) - } - } - 130.toByte() -> { - return (readValue(buffer) as? List)?.let { - ClientCertPrompt.fromList(it) - } - } - else -> super.readValueOfType(type, buffer) - } - } - override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { - when (value) { - is ClientCertData -> { - stream.write(129) - writeValue(stream, value.toList()) - } - is ClientCertPrompt -> { - stream.write(130) - writeValue(stream, value.toList()) - } - else -> super.writeValue(stream, value) - } - } -} - - -/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ -interface NetworkApi { - fun addCertificate(clientData: ClientCertData, callback: (Result) -> Unit) - fun selectCertificate(promptText: ClientCertPrompt, callback: (Result) -> Unit) - fun removeCertificate(callback: (Result) -> Unit) - fun hasCertificate(): Boolean - fun getClientPointer(): Long - fun setRequestHeaders(headers: Map, serverUrls: List, token: String?) - fun getAppGroupId(): String - - companion object { - /** The codec used by NetworkApi. */ - val codec: MessageCodec by lazy { - NetworkPigeonCodec() - } - /** Sets up an instance of `NetworkApi` to handle messages through the `binaryMessenger`. */ - @JvmOverloads - fun setUp(binaryMessenger: BinaryMessenger, api: NetworkApi?, messageChannelSuffix: String = "") { - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.addCertificate$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val clientDataArg = args[0] as ClientCertData - api.addCertificate(clientDataArg) { result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(NetworkPigeonUtils.wrapError(error)) - } else { - reply.reply(NetworkPigeonUtils.wrapResult(null)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.selectCertificate$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val promptTextArg = args[0] as ClientCertPrompt - api.selectCertificate(promptTextArg) { result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(NetworkPigeonUtils.wrapError(error)) - } else { - reply.reply(NetworkPigeonUtils.wrapResult(null)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.removeCertificate$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - api.removeCertificate{ result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(NetworkPigeonUtils.wrapError(error)) - } else { - reply.reply(NetworkPigeonUtils.wrapResult(null)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.hasCertificate$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - listOf(api.hasCertificate()) - } catch (exception: Throwable) { - NetworkPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.getClientPointer$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - listOf(api.getClientPointer()) - } catch (exception: Throwable) { - NetworkPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.setRequestHeaders$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val headersArg = args[0] as Map - val serverUrlsArg = args[1] as List - val tokenArg = args[2] as String? - val wrapped: List = try { - api.setRequestHeaders(headersArg, serverUrlsArg, tokenArg) - listOf(null) - } catch (exception: Throwable) { - NetworkPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.getAppGroupId$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - listOf(api.getAppGroupId()) - } catch (exception: Throwable) { - NetworkPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - } - } -} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImages.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImages.g.kt deleted file mode 100644 index e741ce07e9..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/LocalImages.g.kt +++ /dev/null @@ -1,140 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") - -package app.alextran.immich.images - -import android.util.Log -import io.flutter.plugin.common.BasicMessageChannel -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MessageCodec -import io.flutter.plugin.common.StandardMethodCodec -import io.flutter.plugin.common.StandardMessageCodec -import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer -private object LocalImagesPigeonUtils { - - fun wrapResult(result: Any?): List { - return listOf(result) - } - - fun wrapError(exception: Throwable): List { - return if (exception is FlutterError) { - listOf( - exception.code, - exception.message, - exception.details - ) - } else { - listOf( - exception.javaClass.simpleName, - exception.toString(), - "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) - ) - } - } -} - -/** - * Error class for passing custom error details to Flutter via a thrown PlatformException. - * @property code The error code. - * @property message The error message. - * @property details The error details. Must be a datatype supported by the api codec. - */ -class FlutterError ( - val code: String, - override val message: String? = null, - val details: Any? = null -) : RuntimeException() -private open class LocalImagesPigeonCodec : StandardMessageCodec() { - override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { - return super.readValueOfType(type, buffer) - } - override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { - super.writeValue(stream, value) - } -} - - -/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ -interface LocalImageApi { - fun requestImage(assetId: String, requestId: Long, width: Long, height: Long, isVideo: Boolean, preferEncoded: Boolean, callback: (Result?>) -> Unit) - fun cancelRequest(requestId: Long) - fun getThumbhash(thumbhash: String, callback: (Result>) -> Unit) - - companion object { - /** The codec used by LocalImageApi. */ - val codec: MessageCodec by lazy { - LocalImagesPigeonCodec() - } - /** Sets up an instance of `LocalImageApi` to handle messages through the `binaryMessenger`. */ - @JvmOverloads - fun setUp(binaryMessenger: BinaryMessenger, api: LocalImageApi?, messageChannelSuffix: String = "") { - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.LocalImageApi.requestImage$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val assetIdArg = args[0] as String - val requestIdArg = args[1] as Long - val widthArg = args[2] as Long - val heightArg = args[3] as Long - val isVideoArg = args[4] as Boolean - val preferEncodedArg = args[5] as Boolean - api.requestImage(assetIdArg, requestIdArg, widthArg, heightArg, isVideoArg, preferEncodedArg) { result: Result?> -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(LocalImagesPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(LocalImagesPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.LocalImageApi.cancelRequest$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val requestIdArg = args[0] as Long - val wrapped: List = try { - api.cancelRequest(requestIdArg) - listOf(null) - } catch (exception: Throwable) { - LocalImagesPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.LocalImageApi.getThumbhash$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val thumbhashArg = args[0] as String - api.getThumbhash(thumbhashArg) { result: Result> -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(LocalImagesPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(LocalImagesPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - } - } -} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImages.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImages.g.kt deleted file mode 100644 index 2b5f4d2f57..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/images/RemoteImages.g.kt +++ /dev/null @@ -1,123 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") - -package app.alextran.immich.images - -import android.util.Log -import io.flutter.plugin.common.BasicMessageChannel -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MessageCodec -import io.flutter.plugin.common.StandardMethodCodec -import io.flutter.plugin.common.StandardMessageCodec -import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer -private object RemoteImagesPigeonUtils { - - fun wrapResult(result: Any?): List { - return listOf(result) - } - - fun wrapError(exception: Throwable): List { - return if (exception is FlutterError) { - listOf( - exception.code, - exception.message, - exception.details - ) - } else { - listOf( - exception.javaClass.simpleName, - exception.toString(), - "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) - ) - } - } -} -private open class RemoteImagesPigeonCodec : StandardMessageCodec() { - override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { - return super.readValueOfType(type, buffer) - } - override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { - super.writeValue(stream, value) - } -} - - -/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ -interface RemoteImageApi { - fun requestImage(url: String, requestId: Long, preferEncoded: Boolean, callback: (Result?>) -> Unit) - fun cancelRequest(requestId: Long) - fun clearCache(callback: (Result) -> Unit) - - companion object { - /** The codec used by RemoteImageApi. */ - val codec: MessageCodec by lazy { - RemoteImagesPigeonCodec() - } - /** Sets up an instance of `RemoteImageApi` to handle messages through the `binaryMessenger`. */ - @JvmOverloads - fun setUp(binaryMessenger: BinaryMessenger, api: RemoteImageApi?, messageChannelSuffix: String = "") { - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.RemoteImageApi.requestImage$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val urlArg = args[0] as String - val requestIdArg = args[1] as Long - val preferEncodedArg = args[2] as Boolean - api.requestImage(urlArg, requestIdArg, preferEncodedArg) { result: Result?> -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(RemoteImagesPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(RemoteImagesPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.RemoteImageApi.cancelRequest$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val requestIdArg = args[0] as Long - val wrapped: List = try { - api.cancelRequest(requestIdArg) - listOf(null) - } catch (exception: Throwable) { - RemoteImagesPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.RemoteImageApi.clearCache$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - api.clearCache{ result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(RemoteImagesPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(RemoteImagesPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - } - } -} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/permission/PermissionApi.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/permission/PermissionApi.g.kt deleted file mode 100644 index 5f7bf806b4..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/permission/PermissionApi.g.kt +++ /dev/null @@ -1,169 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") - -package app.alextran.immich.permission - -import android.util.Log -import io.flutter.plugin.common.BasicMessageChannel -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MessageCodec -import io.flutter.plugin.common.StandardMethodCodec -import io.flutter.plugin.common.StandardMessageCodec -import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer -private object PermissionApiPigeonUtils { - - fun wrapResult(result: Any?): List { - return listOf(result) - } - - fun wrapError(exception: Throwable): List { - return if (exception is FlutterError) { - listOf( - exception.code, - exception.message, - exception.details - ) - } else { - listOf( - exception.javaClass.simpleName, - exception.toString(), - "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) - ) - } - } -} - -/** - * Error class for passing custom error details to Flutter via a thrown PlatformException. - * @property code The error code. - * @property message The error message. - * @property details The error details. Must be a datatype supported by the api codec. - */ -class FlutterError ( - val code: String, - override val message: String? = null, - val details: Any? = null -) : RuntimeException() - -enum class PermissionStatus(val raw: Int) { - GRANTED(0), - DENIED(1), - PERMANENTLY_DENIED(2); - - companion object { - fun ofRaw(raw: Int): PermissionStatus? { - return values().firstOrNull { it.raw == raw } - } - } -} -private open class PermissionApiPigeonCodec : StandardMessageCodec() { - override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { - return when (type) { - 129.toByte() -> { - return (readValue(buffer) as Long?)?.let { - PermissionStatus.ofRaw(it.toInt()) - } - } - else -> super.readValueOfType(type, buffer) - } - } - override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { - when (value) { - is PermissionStatus -> { - stream.write(129) - writeValue(stream, value.raw.toLong()) - } - else -> super.writeValue(stream, value) - } - } -} - - -/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ -interface PermissionApi { - fun isIgnoringBatteryOptimizations(): PermissionStatus - fun hasManageMediaPermission(): Boolean - fun requestManageMediaPermission(callback: (Result) -> Unit) - fun manageMediaPermission(callback: (Result) -> Unit) - - companion object { - /** The codec used by PermissionApi. */ - val codec: MessageCodec by lazy { - PermissionApiPigeonCodec() - } - /** Sets up an instance of `PermissionApi` to handle messages through the `binaryMessenger`. */ - @JvmOverloads - fun setUp(binaryMessenger: BinaryMessenger, api: PermissionApi?, messageChannelSuffix: String = "") { - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.isIgnoringBatteryOptimizations$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - listOf(api.isIgnoringBatteryOptimizations()) - } catch (exception: Throwable) { - PermissionApiPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - listOf(api.hasManageMediaPermission()) - } catch (exception: Throwable) { - PermissionApiPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.requestManageMediaPermission$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - api.requestManageMediaPermission{ result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(PermissionApiPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(PermissionApiPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.manageMediaPermission$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - api.manageMediaPermission{ result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(PermissionApiPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(PermissionApiPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - } - } -} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt deleted file mode 100644 index 02f1cb237d..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt +++ /dev/null @@ -1,823 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") - -package app.alextran.immich.sync - -import android.util.Log -import io.flutter.plugin.common.BasicMessageChannel -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MessageCodec -import io.flutter.plugin.common.StandardMethodCodec -import io.flutter.plugin.common.StandardMessageCodec -import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer -private object MessagesPigeonUtils { - - fun wrapResult(result: Any?): List { - return listOf(result) - } - - fun wrapError(exception: Throwable): List { - return if (exception is FlutterError) { - listOf( - exception.code, - exception.message, - exception.details - ) - } else { - listOf( - exception.javaClass.simpleName, - exception.toString(), - "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) - ) - } - } - fun doubleEquals(a: Double, b: Double): Boolean { - // Normalize -0.0 to 0.0 and handle NaN equality. - return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN()) - } - - fun floatEquals(a: Float, b: Float): Boolean { - // Normalize -0.0 to 0.0 and handle NaN equality. - return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN()) - } - - fun doubleHash(d: Double): Int { - // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. - val normalized = if (d == 0.0) 0.0 else d - val bits = java.lang.Double.doubleToLongBits(normalized) - return (bits xor (bits ushr 32)).toInt() - } - - fun floatHash(f: Float): Int { - // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. - val normalized = if (f == 0.0f) 0.0f else f - return java.lang.Float.floatToIntBits(normalized) - } - - fun deepEquals(a: Any?, b: Any?): Boolean { - if (a === b) { - return true - } - if (a == null || b == null) { - return false - } - if (a is ByteArray && b is ByteArray) { - return a.contentEquals(b) - } - if (a is IntArray && b is IntArray) { - return a.contentEquals(b) - } - if (a is LongArray && b is LongArray) { - return a.contentEquals(b) - } - if (a is DoubleArray && b is DoubleArray) { - if (a.size != b.size) return false - for (i in a.indices) { - if (!doubleEquals(a[i], b[i])) return false - } - return true - } - if (a is FloatArray && b is FloatArray) { - if (a.size != b.size) return false - for (i in a.indices) { - if (!floatEquals(a[i], b[i])) return false - } - return true - } - if (a is Array<*> && b is Array<*>) { - if (a.size != b.size) return false - for (i in a.indices) { - if (!deepEquals(a[i], b[i])) return false - } - return true - } - if (a is List<*> && b is List<*>) { - if (a.size != b.size) return false - val iterA = a.iterator() - val iterB = b.iterator() - while (iterA.hasNext() && iterB.hasNext()) { - if (!deepEquals(iterA.next(), iterB.next())) return false - } - return true - } - if (a is Map<*, *> && b is Map<*, *>) { - if (a.size != b.size) return false - for (entry in a) { - val key = entry.key - var found = false - for (bEntry in b) { - if (deepEquals(key, bEntry.key)) { - if (deepEquals(entry.value, bEntry.value)) { - found = true - break - } else { - return false - } - } - } - if (!found) return false - } - return true - } - if (a is Double && b is Double) { - return doubleEquals(a, b) - } - if (a is Float && b is Float) { - return floatEquals(a, b) - } - return a == b - } - - fun deepHash(value: Any?): Int { - return when (value) { - null -> 0 - is ByteArray -> value.contentHashCode() - is IntArray -> value.contentHashCode() - is LongArray -> value.contentHashCode() - is DoubleArray -> { - var result = 1 - for (item in value) { - result = 31 * result + doubleHash(item) - } - result - } - is FloatArray -> { - var result = 1 - for (item in value) { - result = 31 * result + floatHash(item) - } - result - } - is Array<*> -> { - var result = 1 - for (item in value) { - result = 31 * result + deepHash(item) - } - result - } - is List<*> -> { - var result = 1 - for (item in value) { - result = 31 * result + deepHash(item) - } - result - } - is Map<*, *> -> { - var result = 0 - for (entry in value) { - result += ((deepHash(entry.key) * 31) xor deepHash(entry.value)) - } - result - } - is Double -> doubleHash(value) - is Float -> floatHash(value) - else -> value.hashCode() - } - } - -} - -/** - * Error class for passing custom error details to Flutter via a thrown PlatformException. - * @property code The error code. - * @property message The error message. - * @property details The error details. Must be a datatype supported by the api codec. - */ -class FlutterError ( - val code: String, - override val message: String? = null, - val details: Any? = null -) : RuntimeException() - -enum class PlatformAssetPlaybackStyle(val raw: Int) { - UNKNOWN(0), - IMAGE(1), - VIDEO(2), - IMAGE_ANIMATED(3), - LIVE_PHOTO(4), - VIDEO_LOOPING(5); - - companion object { - fun ofRaw(raw: Int): PlatformAssetPlaybackStyle? { - return values().firstOrNull { it.raw == raw } - } - } -} - -/** Generated class from Pigeon that represents data sent in messages. */ -data class PlatformAsset ( - val id: String, - val name: String, - val type: Long, - val createdAt: Long? = null, - val updatedAt: Long? = null, - val width: Long? = null, - val height: Long? = null, - val durationMs: Long, - val orientation: Long, - val isFavorite: Boolean, - val adjustmentTime: Long? = null, - val latitude: Double? = null, - val longitude: Double? = null, - val playbackStyle: PlatformAssetPlaybackStyle -) - { - companion object { - fun fromList(pigeonVar_list: List): PlatformAsset { - val id = pigeonVar_list[0] as String - val name = pigeonVar_list[1] as String - val type = pigeonVar_list[2] as Long - val createdAt = pigeonVar_list[3] as Long? - val updatedAt = pigeonVar_list[4] as Long? - val width = pigeonVar_list[5] as Long? - val height = pigeonVar_list[6] as Long? - val durationMs = pigeonVar_list[7] as Long - val orientation = pigeonVar_list[8] as Long - val isFavorite = pigeonVar_list[9] as Boolean - val adjustmentTime = pigeonVar_list[10] as Long? - val latitude = pigeonVar_list[11] as Double? - val longitude = pigeonVar_list[12] as Double? - val playbackStyle = pigeonVar_list[13] as PlatformAssetPlaybackStyle - return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationMs, orientation, isFavorite, adjustmentTime, latitude, longitude, playbackStyle) - } - } - fun toList(): List { - return listOf( - id, - name, - type, - createdAt, - updatedAt, - width, - height, - durationMs, - orientation, - isFavorite, - adjustmentTime, - latitude, - longitude, - playbackStyle, - ) - } - override fun equals(other: Any?): Boolean { - if (other == null || other.javaClass != javaClass) { - return false - } - if (this === other) { - return true - } - val other = other as PlatformAsset - return MessagesPigeonUtils.deepEquals(this.id, other.id) && MessagesPigeonUtils.deepEquals(this.name, other.name) && MessagesPigeonUtils.deepEquals(this.type, other.type) && MessagesPigeonUtils.deepEquals(this.createdAt, other.createdAt) && MessagesPigeonUtils.deepEquals(this.updatedAt, other.updatedAt) && MessagesPigeonUtils.deepEquals(this.width, other.width) && MessagesPigeonUtils.deepEquals(this.height, other.height) && MessagesPigeonUtils.deepEquals(this.durationMs, other.durationMs) && MessagesPigeonUtils.deepEquals(this.orientation, other.orientation) && MessagesPigeonUtils.deepEquals(this.isFavorite, other.isFavorite) && MessagesPigeonUtils.deepEquals(this.adjustmentTime, other.adjustmentTime) && MessagesPigeonUtils.deepEquals(this.latitude, other.latitude) && MessagesPigeonUtils.deepEquals(this.longitude, other.longitude) && MessagesPigeonUtils.deepEquals(this.playbackStyle, other.playbackStyle) - } - - override fun hashCode(): Int { - var result = javaClass.hashCode() - result = 31 * result + MessagesPigeonUtils.deepHash(this.id) - result = 31 * result + MessagesPigeonUtils.deepHash(this.name) - result = 31 * result + MessagesPigeonUtils.deepHash(this.type) - result = 31 * result + MessagesPigeonUtils.deepHash(this.createdAt) - result = 31 * result + MessagesPigeonUtils.deepHash(this.updatedAt) - result = 31 * result + MessagesPigeonUtils.deepHash(this.width) - result = 31 * result + MessagesPigeonUtils.deepHash(this.height) - result = 31 * result + MessagesPigeonUtils.deepHash(this.durationMs) - result = 31 * result + MessagesPigeonUtils.deepHash(this.orientation) - result = 31 * result + MessagesPigeonUtils.deepHash(this.isFavorite) - result = 31 * result + MessagesPigeonUtils.deepHash(this.adjustmentTime) - result = 31 * result + MessagesPigeonUtils.deepHash(this.latitude) - result = 31 * result + MessagesPigeonUtils.deepHash(this.longitude) - result = 31 * result + MessagesPigeonUtils.deepHash(this.playbackStyle) - return result - } -} - -/** Generated class from Pigeon that represents data sent in messages. */ -data class PlatformAlbum ( - val id: String, - val name: String, - val updatedAt: Long? = null, - val isCloud: Boolean, - val assetCount: Long -) - { - companion object { - fun fromList(pigeonVar_list: List): PlatformAlbum { - val id = pigeonVar_list[0] as String - val name = pigeonVar_list[1] as String - val updatedAt = pigeonVar_list[2] as Long? - val isCloud = pigeonVar_list[3] as Boolean - val assetCount = pigeonVar_list[4] as Long - return PlatformAlbum(id, name, updatedAt, isCloud, assetCount) - } - } - fun toList(): List { - return listOf( - id, - name, - updatedAt, - isCloud, - assetCount, - ) - } - override fun equals(other: Any?): Boolean { - if (other == null || other.javaClass != javaClass) { - return false - } - if (this === other) { - return true - } - val other = other as PlatformAlbum - return MessagesPigeonUtils.deepEquals(this.id, other.id) && MessagesPigeonUtils.deepEquals(this.name, other.name) && MessagesPigeonUtils.deepEquals(this.updatedAt, other.updatedAt) && MessagesPigeonUtils.deepEquals(this.isCloud, other.isCloud) && MessagesPigeonUtils.deepEquals(this.assetCount, other.assetCount) - } - - override fun hashCode(): Int { - var result = javaClass.hashCode() - result = 31 * result + MessagesPigeonUtils.deepHash(this.id) - result = 31 * result + MessagesPigeonUtils.deepHash(this.name) - result = 31 * result + MessagesPigeonUtils.deepHash(this.updatedAt) - result = 31 * result + MessagesPigeonUtils.deepHash(this.isCloud) - result = 31 * result + MessagesPigeonUtils.deepHash(this.assetCount) - return result - } -} - -/** Generated class from Pigeon that represents data sent in messages. */ -data class SyncDelta ( - val hasChanges: Boolean, - val updates: List, - val deletes: List, - val assetAlbums: Map> -) - { - companion object { - fun fromList(pigeonVar_list: List): SyncDelta { - val hasChanges = pigeonVar_list[0] as Boolean - val updates = pigeonVar_list[1] as List - val deletes = pigeonVar_list[2] as List - val assetAlbums = pigeonVar_list[3] as Map> - return SyncDelta(hasChanges, updates, deletes, assetAlbums) - } - } - fun toList(): List { - return listOf( - hasChanges, - updates, - deletes, - assetAlbums, - ) - } - override fun equals(other: Any?): Boolean { - if (other == null || other.javaClass != javaClass) { - return false - } - if (this === other) { - return true - } - val other = other as SyncDelta - return MessagesPigeonUtils.deepEquals(this.hasChanges, other.hasChanges) && MessagesPigeonUtils.deepEquals(this.updates, other.updates) && MessagesPigeonUtils.deepEquals(this.deletes, other.deletes) && MessagesPigeonUtils.deepEquals(this.assetAlbums, other.assetAlbums) - } - - override fun hashCode(): Int { - var result = javaClass.hashCode() - result = 31 * result + MessagesPigeonUtils.deepHash(this.hasChanges) - result = 31 * result + MessagesPigeonUtils.deepHash(this.updates) - result = 31 * result + MessagesPigeonUtils.deepHash(this.deletes) - result = 31 * result + MessagesPigeonUtils.deepHash(this.assetAlbums) - return result - } -} - -/** Generated class from Pigeon that represents data sent in messages. */ -data class HashResult ( - val assetId: String, - val error: String? = null, - val hash: String? = null -) - { - companion object { - fun fromList(pigeonVar_list: List): HashResult { - val assetId = pigeonVar_list[0] as String - val error = pigeonVar_list[1] as String? - val hash = pigeonVar_list[2] as String? - return HashResult(assetId, error, hash) - } - } - fun toList(): List { - return listOf( - assetId, - error, - hash, - ) - } - override fun equals(other: Any?): Boolean { - if (other == null || other.javaClass != javaClass) { - return false - } - if (this === other) { - return true - } - val other = other as HashResult - return MessagesPigeonUtils.deepEquals(this.assetId, other.assetId) && MessagesPigeonUtils.deepEquals(this.error, other.error) && MessagesPigeonUtils.deepEquals(this.hash, other.hash) - } - - override fun hashCode(): Int { - var result = javaClass.hashCode() - result = 31 * result + MessagesPigeonUtils.deepHash(this.assetId) - result = 31 * result + MessagesPigeonUtils.deepHash(this.error) - result = 31 * result + MessagesPigeonUtils.deepHash(this.hash) - return result - } -} - -/** Generated class from Pigeon that represents data sent in messages. */ -data class CloudIdResult ( - val assetId: String, - val error: String? = null, - val cloudId: String? = null -) - { - companion object { - fun fromList(pigeonVar_list: List): CloudIdResult { - val assetId = pigeonVar_list[0] as String - val error = pigeonVar_list[1] as String? - val cloudId = pigeonVar_list[2] as String? - return CloudIdResult(assetId, error, cloudId) - } - } - fun toList(): List { - return listOf( - assetId, - error, - cloudId, - ) - } - override fun equals(other: Any?): Boolean { - if (other == null || other.javaClass != javaClass) { - return false - } - if (this === other) { - return true - } - val other = other as CloudIdResult - return MessagesPigeonUtils.deepEquals(this.assetId, other.assetId) && MessagesPigeonUtils.deepEquals(this.error, other.error) && MessagesPigeonUtils.deepEquals(this.cloudId, other.cloudId) - } - - override fun hashCode(): Int { - var result = javaClass.hashCode() - result = 31 * result + MessagesPigeonUtils.deepHash(this.assetId) - result = 31 * result + MessagesPigeonUtils.deepHash(this.error) - result = 31 * result + MessagesPigeonUtils.deepHash(this.cloudId) - return result - } -} -private open class MessagesPigeonCodec : StandardMessageCodec() { - override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { - return when (type) { - 129.toByte() -> { - return (readValue(buffer) as Long?)?.let { - PlatformAssetPlaybackStyle.ofRaw(it.toInt()) - } - } - 130.toByte() -> { - return (readValue(buffer) as? List)?.let { - PlatformAsset.fromList(it) - } - } - 131.toByte() -> { - return (readValue(buffer) as? List)?.let { - PlatformAlbum.fromList(it) - } - } - 132.toByte() -> { - return (readValue(buffer) as? List)?.let { - SyncDelta.fromList(it) - } - } - 133.toByte() -> { - return (readValue(buffer) as? List)?.let { - HashResult.fromList(it) - } - } - 134.toByte() -> { - return (readValue(buffer) as? List)?.let { - CloudIdResult.fromList(it) - } - } - else -> super.readValueOfType(type, buffer) - } - } - override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { - when (value) { - is PlatformAssetPlaybackStyle -> { - stream.write(129) - writeValue(stream, value.raw.toLong()) - } - is PlatformAsset -> { - stream.write(130) - writeValue(stream, value.toList()) - } - is PlatformAlbum -> { - stream.write(131) - writeValue(stream, value.toList()) - } - is SyncDelta -> { - stream.write(132) - writeValue(stream, value.toList()) - } - is HashResult -> { - stream.write(133) - writeValue(stream, value.toList()) - } - is CloudIdResult -> { - stream.write(134) - writeValue(stream, value.toList()) - } - else -> super.writeValue(stream, value) - } - } -} - - -/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ -interface NativeSyncApi { - fun shouldFullSync(callback: (Result) -> Unit) - fun getMediaChanges(callback: (Result) -> Unit) - fun checkpointSync() - fun clearSyncCheckpoint() - fun getAssetIdsForAlbum(albumId: String, callback: (Result>) -> Unit) - fun getAlbums(callback: (Result>) -> Unit) - fun getAssetsCountSince(albumId: String, timestamp: Long): Long - fun getAssetsForAlbum(albumId: String, updatedTimeCond: Long?, callback: (Result>) -> Unit) - fun hashAssets(assetIds: List, allowNetworkAccess: Boolean, callback: (Result>) -> Unit) - fun cancelHashing() - fun cancelSync() - fun getTrashedAssets(): Map> - fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result) -> Unit) - fun getCloudIdForAssetIds(assetIds: List): List - - companion object { - /** The codec used by NativeSyncApi. */ - val codec: MessageCodec by lazy { - MessagesPigeonCodec() - } - /** Sets up an instance of `NativeSyncApi` to handle messages through the `binaryMessenger`. */ - @JvmOverloads - fun setUp(binaryMessenger: BinaryMessenger, api: NativeSyncApi?, messageChannelSuffix: String = "") { - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val taskQueue = binaryMessenger.makeBackgroundTaskQueue() - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - api.shouldFullSync{ result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(MessagesPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(MessagesPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - api.getMediaChanges{ result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(MessagesPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(MessagesPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.checkpointSync() - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.clearSyncCheckpoint() - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val albumIdArg = args[0] as String - api.getAssetIdsForAlbum(albumIdArg) { result: Result> -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(MessagesPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(MessagesPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - api.getAlbums{ result: Result> -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(MessagesPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(MessagesPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince$separatedMessageChannelSuffix", codec, taskQueue) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val albumIdArg = args[0] as String - val timestampArg = args[1] as Long - val wrapped: List = try { - listOf(api.getAssetsCountSince(albumIdArg, timestampArg)) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val albumIdArg = args[0] as String - val updatedTimeCondArg = args[1] as Long? - api.getAssetsForAlbum(albumIdArg, updatedTimeCondArg) { result: Result> -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(MessagesPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(MessagesPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashAssets$separatedMessageChannelSuffix", codec, taskQueue) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val assetIdsArg = args[0] as List - val allowNetworkAccessArg = args[1] as Boolean - api.hashAssets(assetIdsArg, allowNetworkAccessArg) { result: Result> -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(MessagesPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(MessagesPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.cancelHashing$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.cancelHashing() - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.cancelSync$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.cancelSync() - listOf(null) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets$separatedMessageChannelSuffix", codec, taskQueue) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - listOf(api.getTrashedAssets()) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.restoreFromTrashById$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val mediaIdArg = args[0] as String - val typeArg = args[1] as Long - api.restoreFromTrashById(mediaIdArg, typeArg) { result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(MessagesPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(MessagesPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$separatedMessageChannelSuffix", codec, taskQueue) - if (api != null) { - channel.setMessageHandler { message, reply -> - val args = message as List - val assetIdsArg = args[0] as List - val wrapped: List = try { - listOf(api.getCloudIdForAssetIds(assetIdsArg)) - } catch (exception: Throwable) { - MessagesPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - } - } -} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/viewintent/ViewIntent.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/viewintent/ViewIntent.g.kt deleted file mode 100644 index 1d5af15cb4..0000000000 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/viewintent/ViewIntent.g.kt +++ /dev/null @@ -1,292 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") - -package app.alextran.immich.viewintent - -import android.util.Log -import io.flutter.plugin.common.BasicMessageChannel -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MessageCodec -import io.flutter.plugin.common.StandardMethodCodec -import io.flutter.plugin.common.StandardMessageCodec -import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer -private object ViewIntentPigeonUtils { - - fun wrapResult(result: Any?): List { - return listOf(result) - } - - fun wrapError(exception: Throwable): List { - return if (exception is FlutterError) { - listOf( - exception.code, - exception.message, - exception.details - ) - } else { - listOf( - exception.javaClass.simpleName, - exception.toString(), - "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception) - ) - } - } - fun doubleEquals(a: Double, b: Double): Boolean { - // Normalize -0.0 to 0.0 and handle NaN equality. - return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN()) - } - - fun floatEquals(a: Float, b: Float): Boolean { - // Normalize -0.0 to 0.0 and handle NaN equality. - return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN()) - } - - fun doubleHash(d: Double): Int { - // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. - val normalized = if (d == 0.0) 0.0 else d - val bits = java.lang.Double.doubleToLongBits(normalized) - return (bits xor (bits ushr 32)).toInt() - } - - fun floatHash(f: Float): Int { - // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. - val normalized = if (f == 0.0f) 0.0f else f - return java.lang.Float.floatToIntBits(normalized) - } - - fun deepEquals(a: Any?, b: Any?): Boolean { - if (a === b) { - return true - } - if (a == null || b == null) { - return false - } - if (a is ByteArray && b is ByteArray) { - return a.contentEquals(b) - } - if (a is IntArray && b is IntArray) { - return a.contentEquals(b) - } - if (a is LongArray && b is LongArray) { - return a.contentEquals(b) - } - if (a is DoubleArray && b is DoubleArray) { - if (a.size != b.size) return false - for (i in a.indices) { - if (!doubleEquals(a[i], b[i])) return false - } - return true - } - if (a is FloatArray && b is FloatArray) { - if (a.size != b.size) return false - for (i in a.indices) { - if (!floatEquals(a[i], b[i])) return false - } - return true - } - if (a is Array<*> && b is Array<*>) { - if (a.size != b.size) return false - for (i in a.indices) { - if (!deepEquals(a[i], b[i])) return false - } - return true - } - if (a is List<*> && b is List<*>) { - if (a.size != b.size) return false - val iterA = a.iterator() - val iterB = b.iterator() - while (iterA.hasNext() && iterB.hasNext()) { - if (!deepEquals(iterA.next(), iterB.next())) return false - } - return true - } - if (a is Map<*, *> && b is Map<*, *>) { - if (a.size != b.size) return false - for (entry in a) { - val key = entry.key - var found = false - for (bEntry in b) { - if (deepEquals(key, bEntry.key)) { - if (deepEquals(entry.value, bEntry.value)) { - found = true - break - } else { - return false - } - } - } - if (!found) return false - } - return true - } - if (a is Double && b is Double) { - return doubleEquals(a, b) - } - if (a is Float && b is Float) { - return floatEquals(a, b) - } - return a == b - } - - fun deepHash(value: Any?): Int { - return when (value) { - null -> 0 - is ByteArray -> value.contentHashCode() - is IntArray -> value.contentHashCode() - is LongArray -> value.contentHashCode() - is DoubleArray -> { - var result = 1 - for (item in value) { - result = 31 * result + doubleHash(item) - } - result - } - is FloatArray -> { - var result = 1 - for (item in value) { - result = 31 * result + floatHash(item) - } - result - } - is Array<*> -> { - var result = 1 - for (item in value) { - result = 31 * result + deepHash(item) - } - result - } - is List<*> -> { - var result = 1 - for (item in value) { - result = 31 * result + deepHash(item) - } - result - } - is Map<*, *> -> { - var result = 0 - for (entry in value) { - result += ((deepHash(entry.key) * 31) xor deepHash(entry.value)) - } - result - } - is Double -> doubleHash(value) - is Float -> floatHash(value) - else -> value.hashCode() - } - } - -} - -/** - * Error class for passing custom error details to Flutter via a thrown PlatformException. - * @property code The error code. - * @property message The error message. - * @property details The error details. Must be a datatype supported by the api codec. - */ -class FlutterError ( - val code: String, - override val message: String? = null, - val details: Any? = null -) : RuntimeException() - -/** Generated class from Pigeon that represents data sent in messages. */ -data class ViewIntentPayload ( - val path: String? = null, - val mimeType: String, - val localAssetId: String? = null -) - { - companion object { - fun fromList(pigeonVar_list: List): ViewIntentPayload { - val path = pigeonVar_list[0] as String? - val mimeType = pigeonVar_list[1] as String - val localAssetId = pigeonVar_list[2] as String? - return ViewIntentPayload(path, mimeType, localAssetId) - } - } - fun toList(): List { - return listOf( - path, - mimeType, - localAssetId, - ) - } - override fun equals(other: Any?): Boolean { - if (other == null || other.javaClass != javaClass) { - return false - } - if (this === other) { - return true - } - val other = other as ViewIntentPayload - return ViewIntentPigeonUtils.deepEquals(this.path, other.path) && ViewIntentPigeonUtils.deepEquals(this.mimeType, other.mimeType) && ViewIntentPigeonUtils.deepEquals(this.localAssetId, other.localAssetId) - } - - override fun hashCode(): Int { - var result = javaClass.hashCode() - result = 31 * result + ViewIntentPigeonUtils.deepHash(this.path) - result = 31 * result + ViewIntentPigeonUtils.deepHash(this.mimeType) - result = 31 * result + ViewIntentPigeonUtils.deepHash(this.localAssetId) - return result - } -} -private open class ViewIntentPigeonCodec : StandardMessageCodec() { - override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { - return when (type) { - 129.toByte() -> { - return (readValue(buffer) as? List)?.let { - ViewIntentPayload.fromList(it) - } - } - else -> super.readValueOfType(type, buffer) - } - } - override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { - when (value) { - is ViewIntentPayload -> { - stream.write(129) - writeValue(stream, value.toList()) - } - else -> super.writeValue(stream, value) - } - } -} - - -/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ -interface ViewIntentHostApi { - fun consumeViewIntent(callback: (Result) -> Unit) - - companion object { - /** The codec used by ViewIntentHostApi. */ - val codec: MessageCodec by lazy { - ViewIntentPigeonCodec() - } - /** Sets up an instance of `ViewIntentHostApi` to handle messages through the `binaryMessenger`. */ - @JvmOverloads - fun setUp(binaryMessenger: BinaryMessenger, api: ViewIntentHostApi?, messageChannelSuffix: String = "") { - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.ViewIntentHostApi.consumeViewIntent$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - api.consumeViewIntent{ result: Result -> - val error = result.exceptionOrNull() - if (error != null) { - reply.reply(ViewIntentPigeonUtils.wrapError(error)) - } else { - val data = result.getOrNull() - reply.reply(ViewIntentPigeonUtils.wrapResult(data)) - } - } - } - } else { - channel.setMessageHandler(null) - } - } - } - } -} diff --git a/mobile/ios/Runner/Background/BackgroundWorker.g.swift b/mobile/ios/Runner/Background/BackgroundWorker.g.swift deleted file mode 100644 index bd01e953f9..0000000000 --- a/mobile/ios/Runner/Background/BackgroundWorker.g.swift +++ /dev/null @@ -1,418 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -import Foundation - -#if os(iOS) - import Flutter -#elseif os(macOS) - import FlutterMacOS -#else - #error("Unsupported platform.") -#endif - -private func wrapResult(_ result: Any?) -> [Any?] { - return [result] -} - -private func wrapError(_ error: Any) -> [Any?] { - if let pigeonError = error as? PigeonError { - return [ - pigeonError.code, - pigeonError.message, - pigeonError.details, - ] - } - if let flutterError = error as? FlutterError { - return [ - flutterError.code, - flutterError.message, - flutterError.details, - ] - } - return [ - "\(error)", - "\(Swift.type(of: error))", - "Stacktrace: \(Thread.callStackSymbols)", - ] -} - -private func createConnectionError(withChannelName channelName: String) -> PigeonError { - return PigeonError(code: "channel-error", message: "Unable to establish connection on channel: '\(channelName)'.", details: "") -} - -private func isNullish(_ value: Any?) -> Bool { - return value is NSNull || value == nil -} - -private func nilOrValue(_ value: Any?) -> T? { - if value is NSNull { return nil } - return value as! T? -} - -private func doubleEqualsBackgroundWorker(_ lhs: Double, _ rhs: Double) -> Bool { - return (lhs.isNaN && rhs.isNaN) || lhs == rhs -} - -private func doubleHashBackgroundWorker(_ value: Double, _ hasher: inout Hasher) { - if value.isNaN { - hasher.combine(0x7FF8000000000000) - } else { - // Normalize -0.0 to 0.0 - hasher.combine(value == 0 ? 0 : value) - } -} - -func deepEqualsBackgroundWorker(_ lhs: Any?, _ rhs: Any?) -> Bool { - let cleanLhs = nilOrValue(lhs) as Any? - let cleanRhs = nilOrValue(rhs) as Any? - switch (cleanLhs, cleanRhs) { - case (nil, nil): - return true - - case (nil, _), (_, nil): - return false - - case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs: - return true - - case is (Void, Void): - return true - - case (let lhsArray, let rhsArray) as ([Any?], [Any?]): - guard lhsArray.count == rhsArray.count else { return false } - for (index, element) in lhsArray.enumerated() { - if !deepEqualsBackgroundWorker(element, rhsArray[index]) { - return false - } - } - return true - - case (let lhsArray, let rhsArray) as ([Double], [Double]): - guard lhsArray.count == rhsArray.count else { return false } - for (index, element) in lhsArray.enumerated() { - if !doubleEqualsBackgroundWorker(element, rhsArray[index]) { - return false - } - } - return true - - case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): - guard lhsDictionary.count == rhsDictionary.count else { return false } - for (lhsKey, lhsValue) in lhsDictionary { - var found = false - for (rhsKey, rhsValue) in rhsDictionary { - if deepEqualsBackgroundWorker(lhsKey, rhsKey) { - if deepEqualsBackgroundWorker(lhsValue, rhsValue) { - found = true - break - } else { - return false - } - } - } - if !found { return false } - } - return true - - case (let lhs as Double, let rhs as Double): - return doubleEqualsBackgroundWorker(lhs, rhs) - - case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable): - return lhsHashable == rhsHashable - - default: - return false - } -} - -func deepHashBackgroundWorker(value: Any?, hasher: inout Hasher) { - let cleanValue = nilOrValue(value) as Any? - if let cleanValue = cleanValue { - if let doubleValue = cleanValue as? Double { - doubleHashBackgroundWorker(doubleValue, &hasher) - } else if let valueList = cleanValue as? [Any?] { - for item in valueList { - deepHashBackgroundWorker(value: item, hasher: &hasher) - } - } else if let valueList = cleanValue as? [Double] { - for item in valueList { - doubleHashBackgroundWorker(item, &hasher) - } - } else if let valueDict = cleanValue as? [AnyHashable: Any?] { - var result = 0 - for (key, value) in valueDict { - var entryKeyHasher = Hasher() - deepHashBackgroundWorker(value: key, hasher: &entryKeyHasher) - var entryValueHasher = Hasher() - deepHashBackgroundWorker(value: value, hasher: &entryValueHasher) - result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize()) - } - hasher.combine(result) - } else if let hashableValue = cleanValue as? AnyHashable { - hasher.combine(hashableValue) - } else { - hasher.combine(String(describing: cleanValue)) - } - } else { - hasher.combine(0) - } -} - - -/// Generated class from Pigeon that represents data sent in messages. -struct BackgroundWorkerSettings: Hashable { - var requiresCharging: Bool - var minimumDelaySeconds: Int64 - - - // swift-format-ignore: AlwaysUseLowerCamelCase - static func fromList(_ pigeonVar_list: [Any?]) -> BackgroundWorkerSettings? { - let requiresCharging = pigeonVar_list[0] as! Bool - let minimumDelaySeconds = pigeonVar_list[1] as! Int64 - - return BackgroundWorkerSettings( - requiresCharging: requiresCharging, - minimumDelaySeconds: minimumDelaySeconds - ) - } - func toList() -> [Any?] { - return [ - requiresCharging, - minimumDelaySeconds, - ] - } - static func == (lhs: BackgroundWorkerSettings, rhs: BackgroundWorkerSettings) -> Bool { - if Swift.type(of: lhs) != Swift.type(of: rhs) { - return false - } - return deepEqualsBackgroundWorker(lhs.requiresCharging, rhs.requiresCharging) && deepEqualsBackgroundWorker(lhs.minimumDelaySeconds, rhs.minimumDelaySeconds) - } - - func hash(into hasher: inout Hasher) { - hasher.combine("BackgroundWorkerSettings") - deepHashBackgroundWorker(value: requiresCharging, hasher: &hasher) - deepHashBackgroundWorker(value: minimumDelaySeconds, hasher: &hasher) - } -} - -private class BackgroundWorkerPigeonCodecReader: FlutterStandardReader { - override func readValue(ofType type: UInt8) -> Any? { - switch type { - case 129: - return BackgroundWorkerSettings.fromList(self.readValue() as! [Any?]) - default: - return super.readValue(ofType: type) - } - } -} - -private class BackgroundWorkerPigeonCodecWriter: FlutterStandardWriter { - override func writeValue(_ value: Any) { - if let value = value as? BackgroundWorkerSettings { - super.writeByte(129) - super.writeValue(value.toList()) - } else { - super.writeValue(value) - } - } -} - -private class BackgroundWorkerPigeonCodecReaderWriter: FlutterStandardReaderWriter { - override func reader(with data: Data) -> FlutterStandardReader { - return BackgroundWorkerPigeonCodecReader(data: data) - } - - override func writer(with data: NSMutableData) -> FlutterStandardWriter { - return BackgroundWorkerPigeonCodecWriter(data: data) - } -} - -class BackgroundWorkerPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { - static let shared = BackgroundWorkerPigeonCodec(readerWriter: BackgroundWorkerPigeonCodecReaderWriter()) -} - -/// Generated protocol from Pigeon that represents a handler of messages from Flutter. -protocol BackgroundWorkerFgHostApi { - func enable() throws - func saveNotificationMessage(title: String, body: String) throws - func configure(settings: BackgroundWorkerSettings) throws - func disable() throws -} - -/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. -class BackgroundWorkerFgHostApiSetup { - static var codec: FlutterStandardMessageCodec { BackgroundWorkerPigeonCodec.shared } - /// Sets up an instance of `BackgroundWorkerFgHostApi` to handle messages through the `binaryMessenger`. - static func setUp(binaryMessenger: FlutterBinaryMessenger, api: BackgroundWorkerFgHostApi?, messageChannelSuffix: String = "") { - let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - let enableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - enableChannel.setMessageHandler { _, reply in - do { - try api.enable() - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - enableChannel.setMessageHandler(nil) - } - let saveNotificationMessageChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.saveNotificationMessage\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - saveNotificationMessageChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let titleArg = args[0] as! String - let bodyArg = args[1] as! String - do { - try api.saveNotificationMessage(title: titleArg, body: bodyArg) - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - saveNotificationMessageChannel.setMessageHandler(nil) - } - let configureChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.configure\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - configureChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let settingsArg = args[0] as! BackgroundWorkerSettings - do { - try api.configure(settings: settingsArg) - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - configureChannel.setMessageHandler(nil) - } - let disableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - disableChannel.setMessageHandler { _, reply in - do { - try api.disable() - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - disableChannel.setMessageHandler(nil) - } - } -} -/// Generated protocol from Pigeon that represents a handler of messages from Flutter. -protocol BackgroundWorkerBgHostApi { - func onInitialized() throws - func close() throws -} - -/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. -class BackgroundWorkerBgHostApiSetup { - static var codec: FlutterStandardMessageCodec { BackgroundWorkerPigeonCodec.shared } - /// Sets up an instance of `BackgroundWorkerBgHostApi` to handle messages through the `binaryMessenger`. - static func setUp(binaryMessenger: FlutterBinaryMessenger, api: BackgroundWorkerBgHostApi?, messageChannelSuffix: String = "") { - let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - let onInitializedChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.onInitialized\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - onInitializedChannel.setMessageHandler { _, reply in - do { - try api.onInitialized() - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - onInitializedChannel.setMessageHandler(nil) - } - let closeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - closeChannel.setMessageHandler { _, reply in - do { - try api.close() - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - closeChannel.setMessageHandler(nil) - } - } -} -/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. -protocol BackgroundWorkerFlutterApiProtocol { - func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result) -> Void) - func onAndroidUpload(maxMinutes maxMinutesArg: Int64?, completion: @escaping (Result) -> Void) - func cancel(completion: @escaping (Result) -> Void) -} -class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol { - private let binaryMessenger: FlutterBinaryMessenger - private let messageChannelSuffix: String - init(binaryMessenger: FlutterBinaryMessenger, messageChannelSuffix: String = "") { - self.binaryMessenger = binaryMessenger - self.messageChannelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - } - var codec: BackgroundWorkerPigeonCodec { - return BackgroundWorkerPigeonCodec.shared - } - func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result) -> Void) { - let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload\(messageChannelSuffix)" - let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage([isRefreshArg, maxSecondsArg] as [Any?]) { response in - guard let listResponse = response as? [Any?] else { - completion(.failure(createConnectionError(withChannelName: channelName))) - return - } - if listResponse.count > 1 { - let code: String = listResponse[0] as! String - let message: String? = nilOrValue(listResponse[1]) - let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) - } else { - completion(.success(())) - } - } - } - func onAndroidUpload(maxMinutes maxMinutesArg: Int64?, completion: @escaping (Result) -> Void) { - let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload\(messageChannelSuffix)" - let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage([maxMinutesArg] as [Any?]) { response in - guard let listResponse = response as? [Any?] else { - completion(.failure(createConnectionError(withChannelName: channelName))) - return - } - if listResponse.count > 1 { - let code: String = listResponse[0] as! String - let message: String? = nilOrValue(listResponse[1]) - let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) - } else { - completion(.success(())) - } - } - } - func cancel(completion: @escaping (Result) -> Void) { - let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.cancel\(messageChannelSuffix)" - let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage(nil) { response in - guard let listResponse = response as? [Any?] else { - completion(.failure(createConnectionError(withChannelName: channelName))) - return - } - if listResponse.count > 1 { - let code: String = listResponse[0] as! String - let message: String? = nilOrValue(listResponse[1]) - let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) - } else { - completion(.success(())) - } - } - } -} diff --git a/mobile/ios/Runner/Connectivity/Connectivity.g.swift b/mobile/ios/Runner/Connectivity/Connectivity.g.swift deleted file mode 100644 index c7aff63e10..0000000000 --- a/mobile/ios/Runner/Connectivity/Connectivity.g.swift +++ /dev/null @@ -1,129 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -import Foundation - -#if os(iOS) - import Flutter -#elseif os(macOS) - import FlutterMacOS -#else - #error("Unsupported platform.") -#endif - -private func wrapResult(_ result: Any?) -> [Any?] { - return [result] -} - -private func wrapError(_ error: Any) -> [Any?] { - if let pigeonError = error as? PigeonError { - return [ - pigeonError.code, - pigeonError.message, - pigeonError.details, - ] - } - if let flutterError = error as? FlutterError { - return [ - flutterError.code, - flutterError.message, - flutterError.details, - ] - } - return [ - "\(error)", - "\(Swift.type(of: error))", - "Stacktrace: \(Thread.callStackSymbols)", - ] -} - -private func isNullish(_ value: Any?) -> Bool { - return value is NSNull || value == nil -} - -private func nilOrValue(_ value: Any?) -> T? { - if value is NSNull { return nil } - return value as! T? -} - - -enum NetworkCapability: Int { - case cellular = 0 - case wifi = 1 - case vpn = 2 - case unmetered = 3 -} - -private class ConnectivityPigeonCodecReader: FlutterStandardReader { - override func readValue(ofType type: UInt8) -> Any? { - switch type { - case 129: - let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) - if let enumResultAsInt = enumResultAsInt { - return NetworkCapability(rawValue: enumResultAsInt) - } - return nil - default: - return super.readValue(ofType: type) - } - } -} - -private class ConnectivityPigeonCodecWriter: FlutterStandardWriter { - override func writeValue(_ value: Any) { - if let value = value as? NetworkCapability { - super.writeByte(129) - super.writeValue(value.rawValue) - } else { - super.writeValue(value) - } - } -} - -private class ConnectivityPigeonCodecReaderWriter: FlutterStandardReaderWriter { - override func reader(with data: Data) -> FlutterStandardReader { - return ConnectivityPigeonCodecReader(data: data) - } - - override func writer(with data: NSMutableData) -> FlutterStandardWriter { - return ConnectivityPigeonCodecWriter(data: data) - } -} - -class ConnectivityPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { - static let shared = ConnectivityPigeonCodec(readerWriter: ConnectivityPigeonCodecReaderWriter()) -} - -/// Generated protocol from Pigeon that represents a handler of messages from Flutter. -protocol ConnectivityApi { - func getCapabilities() throws -> [NetworkCapability] -} - -/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. -class ConnectivityApiSetup { - static var codec: FlutterStandardMessageCodec { ConnectivityPigeonCodec.shared } - /// Sets up an instance of `ConnectivityApi` to handle messages through the `binaryMessenger`. - static func setUp(binaryMessenger: FlutterBinaryMessenger, api: ConnectivityApi?, messageChannelSuffix: String = "") { - let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - #if os(iOS) - let taskQueue = binaryMessenger.makeBackgroundTaskQueue?() - #else - let taskQueue: FlutterTaskQueue? = nil - #endif - let getCapabilitiesChannel = taskQueue == nil - ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) - if let api = api { - getCapabilitiesChannel.setMessageHandler { _, reply in - do { - let result = try api.getCapabilities() - reply(wrapResult(result)) - } catch { - reply(wrapError(error)) - } - } - } else { - getCapabilitiesChannel.setMessageHandler(nil) - } - } -} diff --git a/mobile/ios/Runner/Core/Network.g.swift b/mobile/ios/Runner/Core/Network.g.swift deleted file mode 100644 index 265923d165..0000000000 --- a/mobile/ios/Runner/Core/Network.g.swift +++ /dev/null @@ -1,406 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -import Foundation - -#if os(iOS) - import Flutter -#elseif os(macOS) - import FlutterMacOS -#else - #error("Unsupported platform.") -#endif - -private func wrapResult(_ result: Any?) -> [Any?] { - return [result] -} - -private func wrapError(_ error: Any) -> [Any?] { - if let pigeonError = error as? PigeonError { - return [ - pigeonError.code, - pigeonError.message, - pigeonError.details, - ] - } - if let flutterError = error as? FlutterError { - return [ - flutterError.code, - flutterError.message, - flutterError.details, - ] - } - return [ - "\(error)", - "\(Swift.type(of: error))", - "Stacktrace: \(Thread.callStackSymbols)", - ] -} - -private func isNullish(_ value: Any?) -> Bool { - return value is NSNull || value == nil -} - -private func nilOrValue(_ value: Any?) -> T? { - if value is NSNull { return nil } - return value as! T? -} - -private func doubleEqualsNetwork(_ lhs: Double, _ rhs: Double) -> Bool { - return (lhs.isNaN && rhs.isNaN) || lhs == rhs -} - -private func doubleHashNetwork(_ value: Double, _ hasher: inout Hasher) { - if value.isNaN { - hasher.combine(0x7FF8000000000000) - } else { - // Normalize -0.0 to 0.0 - hasher.combine(value == 0 ? 0 : value) - } -} - -func deepEqualsNetwork(_ lhs: Any?, _ rhs: Any?) -> Bool { - let cleanLhs = nilOrValue(lhs) as Any? - let cleanRhs = nilOrValue(rhs) as Any? - switch (cleanLhs, cleanRhs) { - case (nil, nil): - return true - - case (nil, _), (_, nil): - return false - - case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs: - return true - - case is (Void, Void): - return true - - case (let lhsArray, let rhsArray) as ([Any?], [Any?]): - guard lhsArray.count == rhsArray.count else { return false } - for (index, element) in lhsArray.enumerated() { - if !deepEqualsNetwork(element, rhsArray[index]) { - return false - } - } - return true - - case (let lhsArray, let rhsArray) as ([Double], [Double]): - guard lhsArray.count == rhsArray.count else { return false } - for (index, element) in lhsArray.enumerated() { - if !doubleEqualsNetwork(element, rhsArray[index]) { - return false - } - } - return true - - case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): - guard lhsDictionary.count == rhsDictionary.count else { return false } - for (lhsKey, lhsValue) in lhsDictionary { - var found = false - for (rhsKey, rhsValue) in rhsDictionary { - if deepEqualsNetwork(lhsKey, rhsKey) { - if deepEqualsNetwork(lhsValue, rhsValue) { - found = true - break - } else { - return false - } - } - } - if !found { return false } - } - return true - - case (let lhs as Double, let rhs as Double): - return doubleEqualsNetwork(lhs, rhs) - - case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable): - return lhsHashable == rhsHashable - - default: - return false - } -} - -func deepHashNetwork(value: Any?, hasher: inout Hasher) { - let cleanValue = nilOrValue(value) as Any? - if let cleanValue = cleanValue { - if let doubleValue = cleanValue as? Double { - doubleHashNetwork(doubleValue, &hasher) - } else if let valueList = cleanValue as? [Any?] { - for item in valueList { - deepHashNetwork(value: item, hasher: &hasher) - } - } else if let valueList = cleanValue as? [Double] { - for item in valueList { - doubleHashNetwork(item, &hasher) - } - } else if let valueDict = cleanValue as? [AnyHashable: Any?] { - var result = 0 - for (key, value) in valueDict { - var entryKeyHasher = Hasher() - deepHashNetwork(value: key, hasher: &entryKeyHasher) - var entryValueHasher = Hasher() - deepHashNetwork(value: value, hasher: &entryValueHasher) - result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize()) - } - hasher.combine(result) - } else if let hashableValue = cleanValue as? AnyHashable { - hasher.combine(hashableValue) - } else { - hasher.combine(String(describing: cleanValue)) - } - } else { - hasher.combine(0) - } -} - - -/// Generated class from Pigeon that represents data sent in messages. -struct ClientCertData: Hashable { - var data: FlutterStandardTypedData - var password: String - - - // swift-format-ignore: AlwaysUseLowerCamelCase - static func fromList(_ pigeonVar_list: [Any?]) -> ClientCertData? { - let data = pigeonVar_list[0] as! FlutterStandardTypedData - let password = pigeonVar_list[1] as! String - - return ClientCertData( - data: data, - password: password - ) - } - func toList() -> [Any?] { - return [ - data, - password, - ] - } - static func == (lhs: ClientCertData, rhs: ClientCertData) -> Bool { - if Swift.type(of: lhs) != Swift.type(of: rhs) { - return false - } - return deepEqualsNetwork(lhs.data, rhs.data) && deepEqualsNetwork(lhs.password, rhs.password) - } - - func hash(into hasher: inout Hasher) { - hasher.combine("ClientCertData") - deepHashNetwork(value: data, hasher: &hasher) - deepHashNetwork(value: password, hasher: &hasher) - } -} - -/// Generated class from Pigeon that represents data sent in messages. -struct ClientCertPrompt: Hashable { - var title: String - var message: String - var cancel: String - var confirm: String - - - // swift-format-ignore: AlwaysUseLowerCamelCase - static func fromList(_ pigeonVar_list: [Any?]) -> ClientCertPrompt? { - let title = pigeonVar_list[0] as! String - let message = pigeonVar_list[1] as! String - let cancel = pigeonVar_list[2] as! String - let confirm = pigeonVar_list[3] as! String - - return ClientCertPrompt( - title: title, - message: message, - cancel: cancel, - confirm: confirm - ) - } - func toList() -> [Any?] { - return [ - title, - message, - cancel, - confirm, - ] - } - static func == (lhs: ClientCertPrompt, rhs: ClientCertPrompt) -> Bool { - if Swift.type(of: lhs) != Swift.type(of: rhs) { - return false - } - return deepEqualsNetwork(lhs.title, rhs.title) && deepEqualsNetwork(lhs.message, rhs.message) && deepEqualsNetwork(lhs.cancel, rhs.cancel) && deepEqualsNetwork(lhs.confirm, rhs.confirm) - } - - func hash(into hasher: inout Hasher) { - hasher.combine("ClientCertPrompt") - deepHashNetwork(value: title, hasher: &hasher) - deepHashNetwork(value: message, hasher: &hasher) - deepHashNetwork(value: cancel, hasher: &hasher) - deepHashNetwork(value: confirm, hasher: &hasher) - } -} - -private class NetworkPigeonCodecReader: FlutterStandardReader { - override func readValue(ofType type: UInt8) -> Any? { - switch type { - case 129: - return ClientCertData.fromList(self.readValue() as! [Any?]) - case 130: - return ClientCertPrompt.fromList(self.readValue() as! [Any?]) - default: - return super.readValue(ofType: type) - } - } -} - -private class NetworkPigeonCodecWriter: FlutterStandardWriter { - override func writeValue(_ value: Any) { - if let value = value as? ClientCertData { - super.writeByte(129) - super.writeValue(value.toList()) - } else if let value = value as? ClientCertPrompt { - super.writeByte(130) - super.writeValue(value.toList()) - } else { - super.writeValue(value) - } - } -} - -private class NetworkPigeonCodecReaderWriter: FlutterStandardReaderWriter { - override func reader(with data: Data) -> FlutterStandardReader { - return NetworkPigeonCodecReader(data: data) - } - - override func writer(with data: NSMutableData) -> FlutterStandardWriter { - return NetworkPigeonCodecWriter(data: data) - } -} - -class NetworkPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { - static let shared = NetworkPigeonCodec(readerWriter: NetworkPigeonCodecReaderWriter()) -} - - -/// Generated protocol from Pigeon that represents a handler of messages from Flutter. -protocol NetworkApi { - func addCertificate(clientData: ClientCertData, completion: @escaping (Result) -> Void) - func selectCertificate(promptText: ClientCertPrompt, completion: @escaping (Result) -> Void) - func removeCertificate(completion: @escaping (Result) -> Void) - func hasCertificate() throws -> Bool - func getClientPointer() throws -> Int64 - func setRequestHeaders(headers: [String: String], serverUrls: [String], token: String?) throws - func getAppGroupId() throws -> String -} - -/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. -class NetworkApiSetup { - static var codec: FlutterStandardMessageCodec { NetworkPigeonCodec.shared } - /// Sets up an instance of `NetworkApi` to handle messages through the `binaryMessenger`. - static func setUp(binaryMessenger: FlutterBinaryMessenger, api: NetworkApi?, messageChannelSuffix: String = "") { - let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - let addCertificateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.addCertificate\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - addCertificateChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let clientDataArg = args[0] as! ClientCertData - api.addCertificate(clientData: clientDataArg) { result in - switch result { - case .success: - reply(wrapResult(nil)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - addCertificateChannel.setMessageHandler(nil) - } - let selectCertificateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.selectCertificate\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - selectCertificateChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let promptTextArg = args[0] as! ClientCertPrompt - api.selectCertificate(promptText: promptTextArg) { result in - switch result { - case .success: - reply(wrapResult(nil)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - selectCertificateChannel.setMessageHandler(nil) - } - let removeCertificateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.removeCertificate\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - removeCertificateChannel.setMessageHandler { _, reply in - api.removeCertificate { result in - switch result { - case .success: - reply(wrapResult(nil)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - removeCertificateChannel.setMessageHandler(nil) - } - let hasCertificateChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.hasCertificate\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - hasCertificateChannel.setMessageHandler { _, reply in - do { - let result = try api.hasCertificate() - reply(wrapResult(result)) - } catch { - reply(wrapError(error)) - } - } - } else { - hasCertificateChannel.setMessageHandler(nil) - } - let getClientPointerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.getClientPointer\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - getClientPointerChannel.setMessageHandler { _, reply in - do { - let result = try api.getClientPointer() - reply(wrapResult(result)) - } catch { - reply(wrapError(error)) - } - } - } else { - getClientPointerChannel.setMessageHandler(nil) - } - let setRequestHeadersChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.setRequestHeaders\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - setRequestHeadersChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let headersArg = args[0] as! [String: String] - let serverUrlsArg = args[1] as! [String] - let tokenArg: String? = nilOrValue(args[2]) - do { - try api.setRequestHeaders(headers: headersArg, serverUrls: serverUrlsArg, token: tokenArg) - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - setRequestHeadersChannel.setMessageHandler(nil) - } - let getAppGroupIdChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.getAppGroupId\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - getAppGroupIdChannel.setMessageHandler { _, reply in - do { - let result = try api.getAppGroupId() - reply(wrapResult(result)) - } catch { - reply(wrapError(error)) - } - } - } else { - getAppGroupIdChannel.setMessageHandler(nil) - } - } -} diff --git a/mobile/ios/Runner/Images/LocalImages.g.swift b/mobile/ios/Runner/Images/LocalImages.g.swift deleted file mode 100644 index b9324260be..0000000000 --- a/mobile/ios/Runner/Images/LocalImages.g.swift +++ /dev/null @@ -1,139 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -import Foundation - -#if os(iOS) - import Flutter -#elseif os(macOS) - import FlutterMacOS -#else - #error("Unsupported platform.") -#endif - -private func wrapResult(_ result: Any?) -> [Any?] { - return [result] -} - -private func wrapError(_ error: Any) -> [Any?] { - if let pigeonError = error as? PigeonError { - return [ - pigeonError.code, - pigeonError.message, - pigeonError.details, - ] - } - if let flutterError = error as? FlutterError { - return [ - flutterError.code, - flutterError.message, - flutterError.details, - ] - } - return [ - "\(error)", - "\(Swift.type(of: error))", - "Stacktrace: \(Thread.callStackSymbols)", - ] -} - -private func isNullish(_ value: Any?) -> Bool { - return value is NSNull || value == nil -} - -private func nilOrValue(_ value: Any?) -> T? { - if value is NSNull { return nil } - return value as! T? -} - - -private class LocalImagesPigeonCodecReader: FlutterStandardReader { -} - -private class LocalImagesPigeonCodecWriter: FlutterStandardWriter { -} - -private class LocalImagesPigeonCodecReaderWriter: FlutterStandardReaderWriter { - override func reader(with data: Data) -> FlutterStandardReader { - return LocalImagesPigeonCodecReader(data: data) - } - - override func writer(with data: NSMutableData) -> FlutterStandardWriter { - return LocalImagesPigeonCodecWriter(data: data) - } -} - -class LocalImagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { - static let shared = LocalImagesPigeonCodec(readerWriter: LocalImagesPigeonCodecReaderWriter()) -} - - -/// Generated protocol from Pigeon that represents a handler of messages from Flutter. -protocol LocalImageApi { - func requestImage(assetId: String, requestId: Int64, width: Int64, height: Int64, isVideo: Bool, preferEncoded: Bool, completion: @escaping (Result<[String: Int64]?, Error>) -> Void) - func cancelRequest(requestId: Int64) throws - func getThumbhash(thumbhash: String, completion: @escaping (Result<[String: Int64], Error>) -> Void) -} - -/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. -class LocalImageApiSetup { - static var codec: FlutterStandardMessageCodec { LocalImagesPigeonCodec.shared } - /// Sets up an instance of `LocalImageApi` to handle messages through the `binaryMessenger`. - static func setUp(binaryMessenger: FlutterBinaryMessenger, api: LocalImageApi?, messageChannelSuffix: String = "") { - let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - let requestImageChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.LocalImageApi.requestImage\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - requestImageChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let assetIdArg = args[0] as! String - let requestIdArg = args[1] as! Int64 - let widthArg = args[2] as! Int64 - let heightArg = args[3] as! Int64 - let isVideoArg = args[4] as! Bool - let preferEncodedArg = args[5] as! Bool - api.requestImage(assetId: assetIdArg, requestId: requestIdArg, width: widthArg, height: heightArg, isVideo: isVideoArg, preferEncoded: preferEncodedArg) { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - requestImageChannel.setMessageHandler(nil) - } - let cancelRequestChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.LocalImageApi.cancelRequest\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - cancelRequestChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let requestIdArg = args[0] as! Int64 - do { - try api.cancelRequest(requestId: requestIdArg) - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - cancelRequestChannel.setMessageHandler(nil) - } - let getThumbhashChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.LocalImageApi.getThumbhash\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - getThumbhashChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let thumbhashArg = args[0] as! String - api.getThumbhash(thumbhash: thumbhashArg) { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - getThumbhashChannel.setMessageHandler(nil) - } - } -} diff --git a/mobile/ios/Runner/Images/RemoteImages.g.swift b/mobile/ios/Runner/Images/RemoteImages.g.swift deleted file mode 100644 index 12eaaeec60..0000000000 --- a/mobile/ios/Runner/Images/RemoteImages.g.swift +++ /dev/null @@ -1,134 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -import Foundation - -#if os(iOS) - import Flutter -#elseif os(macOS) - import FlutterMacOS -#else - #error("Unsupported platform.") -#endif - -private func wrapResult(_ result: Any?) -> [Any?] { - return [result] -} - -private func wrapError(_ error: Any) -> [Any?] { - if let pigeonError = error as? PigeonError { - return [ - pigeonError.code, - pigeonError.message, - pigeonError.details, - ] - } - if let flutterError = error as? FlutterError { - return [ - flutterError.code, - flutterError.message, - flutterError.details, - ] - } - return [ - "\(error)", - "\(Swift.type(of: error))", - "Stacktrace: \(Thread.callStackSymbols)", - ] -} - -private func isNullish(_ value: Any?) -> Bool { - return value is NSNull || value == nil -} - -private func nilOrValue(_ value: Any?) -> T? { - if value is NSNull { return nil } - return value as! T? -} - - -private class RemoteImagesPigeonCodecReader: FlutterStandardReader { -} - -private class RemoteImagesPigeonCodecWriter: FlutterStandardWriter { -} - -private class RemoteImagesPigeonCodecReaderWriter: FlutterStandardReaderWriter { - override func reader(with data: Data) -> FlutterStandardReader { - return RemoteImagesPigeonCodecReader(data: data) - } - - override func writer(with data: NSMutableData) -> FlutterStandardWriter { - return RemoteImagesPigeonCodecWriter(data: data) - } -} - -class RemoteImagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { - static let shared = RemoteImagesPigeonCodec(readerWriter: RemoteImagesPigeonCodecReaderWriter()) -} - - -/// Generated protocol from Pigeon that represents a handler of messages from Flutter. -protocol RemoteImageApi { - func requestImage(url: String, requestId: Int64, preferEncoded: Bool, completion: @escaping (Result<[String: Int64]?, Error>) -> Void) - func cancelRequest(requestId: Int64) throws - func clearCache(completion: @escaping (Result) -> Void) -} - -/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. -class RemoteImageApiSetup { - static var codec: FlutterStandardMessageCodec { RemoteImagesPigeonCodec.shared } - /// Sets up an instance of `RemoteImageApi` to handle messages through the `binaryMessenger`. - static func setUp(binaryMessenger: FlutterBinaryMessenger, api: RemoteImageApi?, messageChannelSuffix: String = "") { - let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - let requestImageChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.RemoteImageApi.requestImage\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - requestImageChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let urlArg = args[0] as! String - let requestIdArg = args[1] as! Int64 - let preferEncodedArg = args[2] as! Bool - api.requestImage(url: urlArg, requestId: requestIdArg, preferEncoded: preferEncodedArg) { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - requestImageChannel.setMessageHandler(nil) - } - let cancelRequestChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.RemoteImageApi.cancelRequest\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - cancelRequestChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let requestIdArg = args[0] as! Int64 - do { - try api.cancelRequest(requestId: requestIdArg) - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - cancelRequestChannel.setMessageHandler(nil) - } - let clearCacheChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.RemoteImageApi.clearCache\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - clearCacheChannel.setMessageHandler { _, reply in - api.clearCache { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - clearCacheChannel.setMessageHandler(nil) - } - } -} diff --git a/mobile/ios/Runner/Permission/PermissionApi.g.swift b/mobile/ios/Runner/Permission/PermissionApi.g.swift deleted file mode 100644 index b9c116f0c5..0000000000 --- a/mobile/ios/Runner/Permission/PermissionApi.g.swift +++ /dev/null @@ -1,168 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -import Foundation - -#if os(iOS) - import Flutter -#elseif os(macOS) - import FlutterMacOS -#else - #error("Unsupported platform.") -#endif - -private func wrapResult(_ result: Any?) -> [Any?] { - return [result] -} - -private func wrapError(_ error: Any) -> [Any?] { - if let pigeonError = error as? PigeonError { - return [ - pigeonError.code, - pigeonError.message, - pigeonError.details, - ] - } - if let flutterError = error as? FlutterError { - return [ - flutterError.code, - flutterError.message, - flutterError.details, - ] - } - return [ - "\(error)", - "\(Swift.type(of: error))", - "Stacktrace: \(Thread.callStackSymbols)", - ] -} - -private func isNullish(_ value: Any?) -> Bool { - return value is NSNull || value == nil -} - -private func nilOrValue(_ value: Any?) -> T? { - if value is NSNull { return nil } - return value as! T? -} - - -enum PermissionStatus: Int { - case granted = 0 - case denied = 1 - case permanentlyDenied = 2 -} - -private class PermissionApiPigeonCodecReader: FlutterStandardReader { - override func readValue(ofType type: UInt8) -> Any? { - switch type { - case 129: - let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) - if let enumResultAsInt = enumResultAsInt { - return PermissionStatus(rawValue: enumResultAsInt) - } - return nil - default: - return super.readValue(ofType: type) - } - } -} - -private class PermissionApiPigeonCodecWriter: FlutterStandardWriter { - override func writeValue(_ value: Any) { - if let value = value as? PermissionStatus { - super.writeByte(129) - super.writeValue(value.rawValue) - } else { - super.writeValue(value) - } - } -} - -private class PermissionApiPigeonCodecReaderWriter: FlutterStandardReaderWriter { - override func reader(with data: Data) -> FlutterStandardReader { - return PermissionApiPigeonCodecReader(data: data) - } - - override func writer(with data: NSMutableData) -> FlutterStandardWriter { - return PermissionApiPigeonCodecWriter(data: data) - } -} - -class PermissionApiPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { - static let shared = PermissionApiPigeonCodec(readerWriter: PermissionApiPigeonCodecReaderWriter()) -} - - -/// Generated protocol from Pigeon that represents a handler of messages from Flutter. -protocol PermissionApi { - func isIgnoringBatteryOptimizations() throws -> PermissionStatus - func hasManageMediaPermission() throws -> Bool - func requestManageMediaPermission(completion: @escaping (Result) -> Void) - func manageMediaPermission(completion: @escaping (Result) -> Void) -} - -/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. -class PermissionApiSetup { - static var codec: FlutterStandardMessageCodec { PermissionApiPigeonCodec.shared } - /// Sets up an instance of `PermissionApi` to handle messages through the `binaryMessenger`. - static func setUp(binaryMessenger: FlutterBinaryMessenger, api: PermissionApi?, messageChannelSuffix: String = "") { - let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - let isIgnoringBatteryOptimizationsChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.PermissionApi.isIgnoringBatteryOptimizations\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - isIgnoringBatteryOptimizationsChannel.setMessageHandler { _, reply in - do { - let result = try api.isIgnoringBatteryOptimizations() - reply(wrapResult(result)) - } catch { - reply(wrapError(error)) - } - } - } else { - isIgnoringBatteryOptimizationsChannel.setMessageHandler(nil) - } - let hasManageMediaPermissionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - hasManageMediaPermissionChannel.setMessageHandler { _, reply in - do { - let result = try api.hasManageMediaPermission() - reply(wrapResult(result)) - } catch { - reply(wrapError(error)) - } - } - } else { - hasManageMediaPermissionChannel.setMessageHandler(nil) - } - let requestManageMediaPermissionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.PermissionApi.requestManageMediaPermission\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - requestManageMediaPermissionChannel.setMessageHandler { _, reply in - api.requestManageMediaPermission { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - requestManageMediaPermissionChannel.setMessageHandler(nil) - } - let manageMediaPermissionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.PermissionApi.manageMediaPermission\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - manageMediaPermissionChannel.setMessageHandler { _, reply in - api.manageMediaPermission { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - manageMediaPermissionChannel.setMessageHandler(nil) - } - } -} diff --git a/mobile/ios/Runner/Sync/Messages.g.swift b/mobile/ios/Runner/Sync/Messages.g.swift deleted file mode 100644 index a752785c5b..0000000000 --- a/mobile/ios/Runner/Sync/Messages.g.swift +++ /dev/null @@ -1,777 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -import Foundation - -#if os(iOS) - import Flutter -#elseif os(macOS) - import FlutterMacOS -#else - #error("Unsupported platform.") -#endif - -/// Error class for passing custom error details to Dart side. -final class PigeonError: Error { - let code: String - let message: String? - let details: Sendable? - - init(code: String, message: String?, details: Sendable?) { - self.code = code - self.message = message - self.details = details - } - - var localizedDescription: String { - return - "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" - } -} - -private func wrapResult(_ result: Any?) -> [Any?] { - return [result] -} - -private func wrapError(_ error: Any) -> [Any?] { - if let pigeonError = error as? PigeonError { - return [ - pigeonError.code, - pigeonError.message, - pigeonError.details, - ] - } - if let flutterError = error as? FlutterError { - return [ - flutterError.code, - flutterError.message, - flutterError.details, - ] - } - return [ - "\(error)", - "\(Swift.type(of: error))", - "Stacktrace: \(Thread.callStackSymbols)", - ] -} - -private func isNullish(_ value: Any?) -> Bool { - return value is NSNull || value == nil -} - -private func nilOrValue(_ value: Any?) -> T? { - if value is NSNull { return nil } - return value as! T? -} - -private func doubleEqualsMessages(_ lhs: Double, _ rhs: Double) -> Bool { - return (lhs.isNaN && rhs.isNaN) || lhs == rhs -} - -private func doubleHashMessages(_ value: Double, _ hasher: inout Hasher) { - if value.isNaN { - hasher.combine(0x7FF8000000000000) - } else { - // Normalize -0.0 to 0.0 - hasher.combine(value == 0 ? 0 : value) - } -} - -func deepEqualsMessages(_ lhs: Any?, _ rhs: Any?) -> Bool { - let cleanLhs = nilOrValue(lhs) as Any? - let cleanRhs = nilOrValue(rhs) as Any? - switch (cleanLhs, cleanRhs) { - case (nil, nil): - return true - - case (nil, _), (_, nil): - return false - - case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs: - return true - - case is (Void, Void): - return true - - case (let lhsArray, let rhsArray) as ([Any?], [Any?]): - guard lhsArray.count == rhsArray.count else { return false } - for (index, element) in lhsArray.enumerated() { - if !deepEqualsMessages(element, rhsArray[index]) { - return false - } - } - return true - - case (let lhsArray, let rhsArray) as ([Double], [Double]): - guard lhsArray.count == rhsArray.count else { return false } - for (index, element) in lhsArray.enumerated() { - if !doubleEqualsMessages(element, rhsArray[index]) { - return false - } - } - return true - - case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): - guard lhsDictionary.count == rhsDictionary.count else { return false } - for (lhsKey, lhsValue) in lhsDictionary { - var found = false - for (rhsKey, rhsValue) in rhsDictionary { - if deepEqualsMessages(lhsKey, rhsKey) { - if deepEqualsMessages(lhsValue, rhsValue) { - found = true - break - } else { - return false - } - } - } - if !found { return false } - } - return true - - case (let lhs as Double, let rhs as Double): - return doubleEqualsMessages(lhs, rhs) - - case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable): - return lhsHashable == rhsHashable - - default: - return false - } -} - -func deepHashMessages(value: Any?, hasher: inout Hasher) { - let cleanValue = nilOrValue(value) as Any? - if let cleanValue = cleanValue { - if let doubleValue = cleanValue as? Double { - doubleHashMessages(doubleValue, &hasher) - } else if let valueList = cleanValue as? [Any?] { - for item in valueList { - deepHashMessages(value: item, hasher: &hasher) - } - } else if let valueList = cleanValue as? [Double] { - for item in valueList { - doubleHashMessages(item, &hasher) - } - } else if let valueDict = cleanValue as? [AnyHashable: Any?] { - var result = 0 - for (key, value) in valueDict { - var entryKeyHasher = Hasher() - deepHashMessages(value: key, hasher: &entryKeyHasher) - var entryValueHasher = Hasher() - deepHashMessages(value: value, hasher: &entryValueHasher) - result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize()) - } - hasher.combine(result) - } else if let hashableValue = cleanValue as? AnyHashable { - hasher.combine(hashableValue) - } else { - hasher.combine(String(describing: cleanValue)) - } - } else { - hasher.combine(0) - } -} - - -enum PlatformAssetPlaybackStyle: Int { - case unknown = 0 - case image = 1 - case video = 2 - case imageAnimated = 3 - case livePhoto = 4 - case videoLooping = 5 -} - -/// Generated class from Pigeon that represents data sent in messages. -struct PlatformAsset: Hashable { - var id: String - var name: String - var type: Int64 - var createdAt: Int64? = nil - var updatedAt: Int64? = nil - var width: Int64? = nil - var height: Int64? = nil - var durationMs: Int64 - var orientation: Int64 - var isFavorite: Bool - var adjustmentTime: Int64? = nil - var latitude: Double? = nil - var longitude: Double? = nil - var playbackStyle: PlatformAssetPlaybackStyle - - - // swift-format-ignore: AlwaysUseLowerCamelCase - static func fromList(_ pigeonVar_list: [Any?]) -> PlatformAsset? { - let id = pigeonVar_list[0] as! String - let name = pigeonVar_list[1] as! String - let type = pigeonVar_list[2] as! Int64 - let createdAt: Int64? = nilOrValue(pigeonVar_list[3]) - let updatedAt: Int64? = nilOrValue(pigeonVar_list[4]) - let width: Int64? = nilOrValue(pigeonVar_list[5]) - let height: Int64? = nilOrValue(pigeonVar_list[6]) - let durationMs = pigeonVar_list[7] as! Int64 - let orientation = pigeonVar_list[8] as! Int64 - let isFavorite = pigeonVar_list[9] as! Bool - let adjustmentTime: Int64? = nilOrValue(pigeonVar_list[10]) - let latitude: Double? = nilOrValue(pigeonVar_list[11]) - let longitude: Double? = nilOrValue(pigeonVar_list[12]) - let playbackStyle = pigeonVar_list[13] as! PlatformAssetPlaybackStyle - - return PlatformAsset( - id: id, - name: name, - type: type, - createdAt: createdAt, - updatedAt: updatedAt, - width: width, - height: height, - durationMs: durationMs, - orientation: orientation, - isFavorite: isFavorite, - adjustmentTime: adjustmentTime, - latitude: latitude, - longitude: longitude, - playbackStyle: playbackStyle - ) - } - func toList() -> [Any?] { - return [ - id, - name, - type, - createdAt, - updatedAt, - width, - height, - durationMs, - orientation, - isFavorite, - adjustmentTime, - latitude, - longitude, - playbackStyle, - ] - } - static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool { - if Swift.type(of: lhs) != Swift.type(of: rhs) { - return false - } - return deepEqualsMessages(lhs.id, rhs.id) && deepEqualsMessages(lhs.name, rhs.name) && deepEqualsMessages(lhs.type, rhs.type) && deepEqualsMessages(lhs.createdAt, rhs.createdAt) && deepEqualsMessages(lhs.updatedAt, rhs.updatedAt) && deepEqualsMessages(lhs.width, rhs.width) && deepEqualsMessages(lhs.height, rhs.height) && deepEqualsMessages(lhs.durationMs, rhs.durationMs) && deepEqualsMessages(lhs.orientation, rhs.orientation) && deepEqualsMessages(lhs.isFavorite, rhs.isFavorite) && deepEqualsMessages(lhs.adjustmentTime, rhs.adjustmentTime) && deepEqualsMessages(lhs.latitude, rhs.latitude) && deepEqualsMessages(lhs.longitude, rhs.longitude) && deepEqualsMessages(lhs.playbackStyle, rhs.playbackStyle) - } - - func hash(into hasher: inout Hasher) { - hasher.combine("PlatformAsset") - deepHashMessages(value: id, hasher: &hasher) - deepHashMessages(value: name, hasher: &hasher) - deepHashMessages(value: type, hasher: &hasher) - deepHashMessages(value: createdAt, hasher: &hasher) - deepHashMessages(value: updatedAt, hasher: &hasher) - deepHashMessages(value: width, hasher: &hasher) - deepHashMessages(value: height, hasher: &hasher) - deepHashMessages(value: durationMs, hasher: &hasher) - deepHashMessages(value: orientation, hasher: &hasher) - deepHashMessages(value: isFavorite, hasher: &hasher) - deepHashMessages(value: adjustmentTime, hasher: &hasher) - deepHashMessages(value: latitude, hasher: &hasher) - deepHashMessages(value: longitude, hasher: &hasher) - deepHashMessages(value: playbackStyle, hasher: &hasher) - } -} - -/// Generated class from Pigeon that represents data sent in messages. -struct PlatformAlbum: Hashable { - var id: String - var name: String - var updatedAt: Int64? = nil - var isCloud: Bool - var assetCount: Int64 - - - // swift-format-ignore: AlwaysUseLowerCamelCase - static func fromList(_ pigeonVar_list: [Any?]) -> PlatformAlbum? { - let id = pigeonVar_list[0] as! String - let name = pigeonVar_list[1] as! String - let updatedAt: Int64? = nilOrValue(pigeonVar_list[2]) - let isCloud = pigeonVar_list[3] as! Bool - let assetCount = pigeonVar_list[4] as! Int64 - - return PlatformAlbum( - id: id, - name: name, - updatedAt: updatedAt, - isCloud: isCloud, - assetCount: assetCount - ) - } - func toList() -> [Any?] { - return [ - id, - name, - updatedAt, - isCloud, - assetCount, - ] - } - static func == (lhs: PlatformAlbum, rhs: PlatformAlbum) -> Bool { - if Swift.type(of: lhs) != Swift.type(of: rhs) { - return false - } - return deepEqualsMessages(lhs.id, rhs.id) && deepEqualsMessages(lhs.name, rhs.name) && deepEqualsMessages(lhs.updatedAt, rhs.updatedAt) && deepEqualsMessages(lhs.isCloud, rhs.isCloud) && deepEqualsMessages(lhs.assetCount, rhs.assetCount) - } - - func hash(into hasher: inout Hasher) { - hasher.combine("PlatformAlbum") - deepHashMessages(value: id, hasher: &hasher) - deepHashMessages(value: name, hasher: &hasher) - deepHashMessages(value: updatedAt, hasher: &hasher) - deepHashMessages(value: isCloud, hasher: &hasher) - deepHashMessages(value: assetCount, hasher: &hasher) - } -} - -/// Generated class from Pigeon that represents data sent in messages. -struct SyncDelta: Hashable { - var hasChanges: Bool - var updates: [PlatformAsset] - var deletes: [String] - var assetAlbums: [String: [String]] - - - // swift-format-ignore: AlwaysUseLowerCamelCase - static func fromList(_ pigeonVar_list: [Any?]) -> SyncDelta? { - let hasChanges = pigeonVar_list[0] as! Bool - let updates = pigeonVar_list[1] as! [PlatformAsset] - let deletes = pigeonVar_list[2] as! [String] - let assetAlbums = pigeonVar_list[3] as! [String: [String]] - - return SyncDelta( - hasChanges: hasChanges, - updates: updates, - deletes: deletes, - assetAlbums: assetAlbums - ) - } - func toList() -> [Any?] { - return [ - hasChanges, - updates, - deletes, - assetAlbums, - ] - } - static func == (lhs: SyncDelta, rhs: SyncDelta) -> Bool { - if Swift.type(of: lhs) != Swift.type(of: rhs) { - return false - } - return deepEqualsMessages(lhs.hasChanges, rhs.hasChanges) && deepEqualsMessages(lhs.updates, rhs.updates) && deepEqualsMessages(lhs.deletes, rhs.deletes) && deepEqualsMessages(lhs.assetAlbums, rhs.assetAlbums) - } - - func hash(into hasher: inout Hasher) { - hasher.combine("SyncDelta") - deepHashMessages(value: hasChanges, hasher: &hasher) - deepHashMessages(value: updates, hasher: &hasher) - deepHashMessages(value: deletes, hasher: &hasher) - deepHashMessages(value: assetAlbums, hasher: &hasher) - } -} - -/// Generated class from Pigeon that represents data sent in messages. -struct HashResult: Hashable { - var assetId: String - var error: String? = nil - var hash: String? = nil - - - // swift-format-ignore: AlwaysUseLowerCamelCase - static func fromList(_ pigeonVar_list: [Any?]) -> HashResult? { - let assetId = pigeonVar_list[0] as! String - let error: String? = nilOrValue(pigeonVar_list[1]) - let hash: String? = nilOrValue(pigeonVar_list[2]) - - return HashResult( - assetId: assetId, - error: error, - hash: hash - ) - } - func toList() -> [Any?] { - return [ - assetId, - error, - hash, - ] - } - static func == (lhs: HashResult, rhs: HashResult) -> Bool { - if Swift.type(of: lhs) != Swift.type(of: rhs) { - return false - } - return deepEqualsMessages(lhs.assetId, rhs.assetId) && deepEqualsMessages(lhs.error, rhs.error) && deepEqualsMessages(lhs.hash, rhs.hash) - } - - func hash(into hasher: inout Hasher) { - hasher.combine("HashResult") - deepHashMessages(value: assetId, hasher: &hasher) - deepHashMessages(value: error, hasher: &hasher) - deepHashMessages(value: hash, hasher: &hasher) - } -} - -/// Generated class from Pigeon that represents data sent in messages. -struct CloudIdResult: Hashable { - var assetId: String - var error: String? = nil - var cloudId: String? = nil - - - // swift-format-ignore: AlwaysUseLowerCamelCase - static func fromList(_ pigeonVar_list: [Any?]) -> CloudIdResult? { - let assetId = pigeonVar_list[0] as! String - let error: String? = nilOrValue(pigeonVar_list[1]) - let cloudId: String? = nilOrValue(pigeonVar_list[2]) - - return CloudIdResult( - assetId: assetId, - error: error, - cloudId: cloudId - ) - } - func toList() -> [Any?] { - return [ - assetId, - error, - cloudId, - ] - } - static func == (lhs: CloudIdResult, rhs: CloudIdResult) -> Bool { - if Swift.type(of: lhs) != Swift.type(of: rhs) { - return false - } - return deepEqualsMessages(lhs.assetId, rhs.assetId) && deepEqualsMessages(lhs.error, rhs.error) && deepEqualsMessages(lhs.cloudId, rhs.cloudId) - } - - func hash(into hasher: inout Hasher) { - hasher.combine("CloudIdResult") - deepHashMessages(value: assetId, hasher: &hasher) - deepHashMessages(value: error, hasher: &hasher) - deepHashMessages(value: cloudId, hasher: &hasher) - } -} - -private class MessagesPigeonCodecReader: FlutterStandardReader { - override func readValue(ofType type: UInt8) -> Any? { - switch type { - case 129: - let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?) - if let enumResultAsInt = enumResultAsInt { - return PlatformAssetPlaybackStyle(rawValue: enumResultAsInt) - } - return nil - case 130: - return PlatformAsset.fromList(self.readValue() as! [Any?]) - case 131: - return PlatformAlbum.fromList(self.readValue() as! [Any?]) - case 132: - return SyncDelta.fromList(self.readValue() as! [Any?]) - case 133: - return HashResult.fromList(self.readValue() as! [Any?]) - case 134: - return CloudIdResult.fromList(self.readValue() as! [Any?]) - default: - return super.readValue(ofType: type) - } - } -} - -private class MessagesPigeonCodecWriter: FlutterStandardWriter { - override func writeValue(_ value: Any) { - if let value = value as? PlatformAssetPlaybackStyle { - super.writeByte(129) - super.writeValue(value.rawValue) - } else if let value = value as? PlatformAsset { - super.writeByte(130) - super.writeValue(value.toList()) - } else if let value = value as? PlatformAlbum { - super.writeByte(131) - super.writeValue(value.toList()) - } else if let value = value as? SyncDelta { - super.writeByte(132) - super.writeValue(value.toList()) - } else if let value = value as? HashResult { - super.writeByte(133) - super.writeValue(value.toList()) - } else if let value = value as? CloudIdResult { - super.writeByte(134) - super.writeValue(value.toList()) - } else { - super.writeValue(value) - } - } -} - -private class MessagesPigeonCodecReaderWriter: FlutterStandardReaderWriter { - override func reader(with data: Data) -> FlutterStandardReader { - return MessagesPigeonCodecReader(data: data) - } - - override func writer(with data: NSMutableData) -> FlutterStandardWriter { - return MessagesPigeonCodecWriter(data: data) - } -} - -class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { - static let shared = MessagesPigeonCodec(readerWriter: MessagesPigeonCodecReaderWriter()) -} - - -/// Generated protocol from Pigeon that represents a handler of messages from Flutter. -protocol NativeSyncApi { - func shouldFullSync(completion: @escaping (Result) -> Void) - func getMediaChanges(completion: @escaping (Result) -> Void) - func checkpointSync() throws - func clearSyncCheckpoint() throws - func getAssetIdsForAlbum(albumId: String, completion: @escaping (Result<[String], Error>) -> Void) - func getAlbums(completion: @escaping (Result<[PlatformAlbum], Error>) -> Void) - func getAssetsCountSince(albumId: String, timestamp: Int64) throws -> Int64 - func getAssetsForAlbum(albumId: String, updatedTimeCond: Int64?, completion: @escaping (Result<[PlatformAsset], Error>) -> Void) - func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void) - func cancelHashing() throws - func cancelSync() throws - func getTrashedAssets() throws -> [String: [PlatformAsset]] - func restoreFromTrashById(mediaId: String, type: Int64, completion: @escaping (Result) -> Void) - func getCloudIdForAssetIds(assetIds: [String]) throws -> [CloudIdResult] -} - -/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. -class NativeSyncApiSetup { - static var codec: FlutterStandardMessageCodec { MessagesPigeonCodec.shared } - /// Sets up an instance of `NativeSyncApi` to handle messages through the `binaryMessenger`. - static func setUp(binaryMessenger: FlutterBinaryMessenger, api: NativeSyncApi?, messageChannelSuffix: String = "") { - let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - #if os(iOS) - let taskQueue = binaryMessenger.makeBackgroundTaskQueue?() - #else - let taskQueue: FlutterTaskQueue? = nil - #endif - let shouldFullSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - shouldFullSyncChannel.setMessageHandler { _, reply in - api.shouldFullSync { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - shouldFullSyncChannel.setMessageHandler(nil) - } - let getMediaChangesChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - getMediaChangesChannel.setMessageHandler { _, reply in - api.getMediaChanges { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - getMediaChangesChannel.setMessageHandler(nil) - } - let checkpointSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - checkpointSyncChannel.setMessageHandler { _, reply in - do { - try api.checkpointSync() - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - checkpointSyncChannel.setMessageHandler(nil) - } - let clearSyncCheckpointChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - clearSyncCheckpointChannel.setMessageHandler { _, reply in - do { - try api.clearSyncCheckpoint() - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - clearSyncCheckpointChannel.setMessageHandler(nil) - } - let getAssetIdsForAlbumChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - getAssetIdsForAlbumChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let albumIdArg = args[0] as! String - api.getAssetIdsForAlbum(albumId: albumIdArg) { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - getAssetIdsForAlbumChannel.setMessageHandler(nil) - } - let getAlbumsChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - getAlbumsChannel.setMessageHandler { _, reply in - api.getAlbums { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - getAlbumsChannel.setMessageHandler(nil) - } - let getAssetsCountSinceChannel = taskQueue == nil - ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) - if let api = api { - getAssetsCountSinceChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let albumIdArg = args[0] as! String - let timestampArg = args[1] as! Int64 - do { - let result = try api.getAssetsCountSince(albumId: albumIdArg, timestamp: timestampArg) - reply(wrapResult(result)) - } catch { - reply(wrapError(error)) - } - } - } else { - getAssetsCountSinceChannel.setMessageHandler(nil) - } - let getAssetsForAlbumChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - getAssetsForAlbumChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let albumIdArg = args[0] as! String - let updatedTimeCondArg: Int64? = nilOrValue(args[1]) - api.getAssetsForAlbum(albumId: albumIdArg, updatedTimeCond: updatedTimeCondArg) { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - getAssetsForAlbumChannel.setMessageHandler(nil) - } - let hashAssetsChannel = taskQueue == nil - ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashAssets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashAssets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) - if let api = api { - hashAssetsChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let assetIdsArg = args[0] as! [String] - let allowNetworkAccessArg = args[1] as! Bool - api.hashAssets(assetIds: assetIdsArg, allowNetworkAccess: allowNetworkAccessArg) { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - hashAssetsChannel.setMessageHandler(nil) - } - let cancelHashingChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.cancelHashing\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - cancelHashingChannel.setMessageHandler { _, reply in - do { - try api.cancelHashing() - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - cancelHashingChannel.setMessageHandler(nil) - } - let cancelSyncChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.cancelSync\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - cancelSyncChannel.setMessageHandler { _, reply in - do { - try api.cancelSync() - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - cancelSyncChannel.setMessageHandler(nil) - } - let getTrashedAssetsChannel = taskQueue == nil - ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) - if let api = api { - getTrashedAssetsChannel.setMessageHandler { _, reply in - do { - let result = try api.getTrashedAssets() - reply(wrapResult(result)) - } catch { - reply(wrapError(error)) - } - } - } else { - getTrashedAssetsChannel.setMessageHandler(nil) - } - let restoreFromTrashByIdChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.restoreFromTrashById\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - restoreFromTrashByIdChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let mediaIdArg = args[0] as! String - let typeArg = args[1] as! Int64 - api.restoreFromTrashById(mediaId: mediaIdArg, type: typeArg) { result in - switch result { - case .success(let res): - reply(wrapResult(res)) - case .failure(let error): - reply(wrapError(error)) - } - } - } - } else { - restoreFromTrashByIdChannel.setMessageHandler(nil) - } - let getCloudIdForAssetIdsChannel = taskQueue == nil - ? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - : FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue) - if let api = api { - getCloudIdForAssetIdsChannel.setMessageHandler { message, reply in - let args = message as! [Any?] - let assetIdsArg = args[0] as! [String] - do { - let result = try api.getCloudIdForAssetIds(assetIds: assetIdsArg) - reply(wrapResult(result)) - } catch { - reply(wrapError(error)) - } - } - } else { - getCloudIdForAssetIdsChannel.setMessageHandler(nil) - } - } -} diff --git a/mobile/lib/platform/background_worker_api.g.dart b/mobile/lib/platform/background_worker_api.g.dart deleted file mode 100644 index 34f4c41b48..0000000000 --- a/mobile/lib/platform/background_worker_api.g.dart +++ /dev/null @@ -1,365 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: unused_import, unused_shown_name -// ignore_for_file: type=lint - -import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List; - -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; - -Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); - } else if (replyList.length > 1) { - throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); - } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } - return replyList.firstOrNull; -} - -List wrapResponse({Object? result, PlatformException? error, bool empty = false}) { - if (empty) { - return []; - } - if (error == null) { - return [result]; - } - return [error.code, error.message, error.details]; -} - -bool _deepEquals(Object? a, Object? b) { - if (identical(a, b)) { - return true; - } - if (a is double && b is double) { - if (a.isNaN && b.isNaN) { - return true; - } - return a == b; - } - if (a is List && b is List) { - return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); - } - if (a is Map && b is Map) { - if (a.length != b.length) { - return false; - } - for (final MapEntry entryA in a.entries) { - bool found = false; - for (final MapEntry entryB in b.entries) { - if (_deepEquals(entryA.key, entryB.key)) { - if (_deepEquals(entryA.value, entryB.value)) { - found = true; - break; - } else { - return false; - } - } - } - if (!found) { - return false; - } - } - return true; - } - return a == b; -} - -int _deepHash(Object? value) { - if (value is List) { - return Object.hashAll(value.map(_deepHash)); - } - if (value is Map) { - int result = 0; - for (final MapEntry entry in value.entries) { - result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value); - } - return result; - } - if (value is double && value.isNaN) { - // Normalize NaN to a consistent hash. - return 0x7FF8000000000000.hashCode; - } - if (value is double && value == 0.0) { - // Normalize -0.0 to 0.0 so they have the same hash code. - return 0.0.hashCode; - } - return value.hashCode; -} - -class BackgroundWorkerSettings { - BackgroundWorkerSettings({required this.requiresCharging, required this.minimumDelaySeconds}); - - bool requiresCharging; - - int minimumDelaySeconds; - - List _toList() { - return [requiresCharging, minimumDelaySeconds]; - } - - Object encode() { - return _toList(); - } - - static BackgroundWorkerSettings decode(Object result) { - result as List; - return BackgroundWorkerSettings(requiresCharging: result[0]! as bool, minimumDelaySeconds: result[1]! as int); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - bool operator ==(Object other) { - if (other is! BackgroundWorkerSettings || other.runtimeType != runtimeType) { - return false; - } - if (identical(this, other)) { - return true; - } - return _deepEquals(requiresCharging, other.requiresCharging) && - _deepEquals(minimumDelaySeconds, other.minimumDelaySeconds); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => _deepHash([runtimeType, ..._toList()]); -} - -class _PigeonCodec extends StandardMessageCodec { - const _PigeonCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is int) { - buffer.putUint8(4); - buffer.putInt64(value); - } else if (value is BackgroundWorkerSettings) { - buffer.putUint8(129); - writeValue(buffer, value.encode()); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 129: - return BackgroundWorkerSettings.decode(readValue(buffer)!); - default: - return super.readValueOfType(type, buffer); - } - } -} - -class BackgroundWorkerFgHostApi { - /// Constructor for [BackgroundWorkerFgHostApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - BackgroundWorkerFgHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; - - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - final String pigeonVar_messageChannelSuffix; - - Future enable() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future saveNotificationMessage(String title, String body) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.saveNotificationMessage$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([title, body]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future configure(BackgroundWorkerSettings settings) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.configure$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([settings]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future disable() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } -} - -class BackgroundWorkerBgHostApi { - /// Constructor for [BackgroundWorkerBgHostApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - BackgroundWorkerBgHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; - - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - final String pigeonVar_messageChannelSuffix; - - Future onInitialized() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.onInitialized$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future close() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.close$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } -} - -abstract class BackgroundWorkerFlutterApi { - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - Future onIosUpload(bool isRefresh, int? maxSeconds); - - Future onAndroidUpload(int? maxMinutes); - - Future cancel(); - - static void setUp( - BackgroundWorkerFlutterApi? api, { - BinaryMessenger? binaryMessenger, - String messageChannelSuffix = '', - }) { - messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - { - final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload$messageChannelSuffix', - pigeonChannelCodec, - binaryMessenger: binaryMessenger, - ); - if (api == null) { - pigeonVar_channel.setMessageHandler(null); - } else { - pigeonVar_channel.setMessageHandler((Object? message) async { - final List args = message! as List; - final bool arg_isRefresh = args[0]! as bool; - final int? arg_maxSeconds = args[1] as int?; - try { - await api.onIosUpload(arg_isRefresh, arg_maxSeconds); - return wrapResponse(empty: true); - } on PlatformException catch (e) { - return wrapResponse(error: e); - } catch (e) { - return wrapResponse( - error: PlatformException(code: 'error', message: e.toString()), - ); - } - }); - } - } - { - final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload$messageChannelSuffix', - pigeonChannelCodec, - binaryMessenger: binaryMessenger, - ); - if (api == null) { - pigeonVar_channel.setMessageHandler(null); - } else { - pigeonVar_channel.setMessageHandler((Object? message) async { - final List args = message! as List; - final int? arg_maxMinutes = args[0] as int?; - try { - await api.onAndroidUpload(arg_maxMinutes); - return wrapResponse(empty: true); - } on PlatformException catch (e) { - return wrapResponse(error: e); - } catch (e) { - return wrapResponse( - error: PlatformException(code: 'error', message: e.toString()), - ); - } - }); - } - } - { - final pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.cancel$messageChannelSuffix', - pigeonChannelCodec, - binaryMessenger: binaryMessenger, - ); - if (api == null) { - pigeonVar_channel.setMessageHandler(null); - } else { - pigeonVar_channel.setMessageHandler((Object? message) async { - try { - await api.cancel(); - return wrapResponse(empty: true); - } on PlatformException catch (e) { - return wrapResponse(error: e); - } catch (e) { - return wrapResponse( - error: PlatformException(code: 'error', message: e.toString()), - ); - } - }); - } - } - } -} diff --git a/mobile/lib/platform/background_worker_lock_api.g.dart b/mobile/lib/platform/background_worker_lock_api.g.dart deleted file mode 100644 index c7836c4c69..0000000000 --- a/mobile/lib/platform/background_worker_lock_api.g.dart +++ /dev/null @@ -1,90 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: unused_import, unused_shown_name -// ignore_for_file: type=lint - -import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List; - -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; - -Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); - } else if (replyList.length > 1) { - throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); - } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } - return replyList.firstOrNull; -} - -class _PigeonCodec extends StandardMessageCodec { - const _PigeonCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is int) { - buffer.putUint8(4); - buffer.putInt64(value); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - default: - return super.readValueOfType(type, buffer); - } - } -} - -class BackgroundWorkerLockApi { - /// Constructor for [BackgroundWorkerLockApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - BackgroundWorkerLockApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; - - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - final String pigeonVar_messageChannelSuffix; - - Future lock() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.lock$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future unlock() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerLockApi.unlock$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } -} diff --git a/mobile/lib/platform/connectivity_api.g.dart b/mobile/lib/platform/connectivity_api.g.dart deleted file mode 100644 index 8cf8979532..0000000000 --- a/mobile/lib/platform/connectivity_api.g.dart +++ /dev/null @@ -1,89 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: unused_import, unused_shown_name -// ignore_for_file: type=lint - -import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List; - -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; - -Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); - } else if (replyList.length > 1) { - throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); - } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } - return replyList.firstOrNull; -} - -enum NetworkCapability { cellular, wifi, vpn, unmetered } - -class _PigeonCodec extends StandardMessageCodec { - const _PigeonCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is int) { - buffer.putUint8(4); - buffer.putInt64(value); - } else if (value is NetworkCapability) { - buffer.putUint8(129); - writeValue(buffer, value.index); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 129: - final value = readValue(buffer) as int?; - return value == null ? null : NetworkCapability.values[value]; - default: - return super.readValueOfType(type, buffer); - } - } -} - -class ConnectivityApi { - /// Constructor for [ConnectivityApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - ConnectivityApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; - - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - final String pigeonVar_messageChannelSuffix; - - Future> getCapabilities() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.ConnectivityApi.getCapabilities$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return (pigeonVar_replyValue! as List).cast(); - } -} diff --git a/mobile/lib/platform/local_image_api.g.dart b/mobile/lib/platform/local_image_api.g.dart deleted file mode 100644 index fbd0876735..0000000000 --- a/mobile/lib/platform/local_image_api.g.dart +++ /dev/null @@ -1,128 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: unused_import, unused_shown_name -// ignore_for_file: type=lint - -import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List; - -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; - -Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); - } else if (replyList.length > 1) { - throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); - } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } - return replyList.firstOrNull; -} - -class _PigeonCodec extends StandardMessageCodec { - const _PigeonCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is int) { - buffer.putUint8(4); - buffer.putInt64(value); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - default: - return super.readValueOfType(type, buffer); - } - } -} - -class LocalImageApi { - /// Constructor for [LocalImageApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - LocalImageApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; - - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - final String pigeonVar_messageChannelSuffix; - - Future?> requestImage( - String assetId, { - required int requestId, - required int width, - required int height, - required bool isVideo, - required bool preferEncoded, - }) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.LocalImageApi.requestImage$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([ - assetId, - requestId, - width, - height, - isVideo, - preferEncoded, - ]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ); - return (pigeonVar_replyValue as Map?)?.cast(); - } - - Future cancelRequest(int requestId) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.LocalImageApi.cancelRequest$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([requestId]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future> getThumbhash(String thumbhash) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.LocalImageApi.getThumbhash$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([thumbhash]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return (pigeonVar_replyValue! as Map).cast(); - } -} diff --git a/mobile/lib/platform/native_sync_api.g.dart b/mobile/lib/platform/native_sync_api.g.dart deleted file mode 100644 index bd979af87b..0000000000 --- a/mobile/lib/platform/native_sync_api.g.dart +++ /dev/null @@ -1,708 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: unused_import, unused_shown_name -// ignore_for_file: type=lint - -import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List; - -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; - -Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); - } else if (replyList.length > 1) { - throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); - } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } - return replyList.firstOrNull; -} - -bool _deepEquals(Object? a, Object? b) { - if (identical(a, b)) { - return true; - } - if (a is double && b is double) { - if (a.isNaN && b.isNaN) { - return true; - } - return a == b; - } - if (a is List && b is List) { - return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); - } - if (a is Map && b is Map) { - if (a.length != b.length) { - return false; - } - for (final MapEntry entryA in a.entries) { - bool found = false; - for (final MapEntry entryB in b.entries) { - if (_deepEquals(entryA.key, entryB.key)) { - if (_deepEquals(entryA.value, entryB.value)) { - found = true; - break; - } else { - return false; - } - } - } - if (!found) { - return false; - } - } - return true; - } - return a == b; -} - -int _deepHash(Object? value) { - if (value is List) { - return Object.hashAll(value.map(_deepHash)); - } - if (value is Map) { - int result = 0; - for (final MapEntry entry in value.entries) { - result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value); - } - return result; - } - if (value is double && value.isNaN) { - // Normalize NaN to a consistent hash. - return 0x7FF8000000000000.hashCode; - } - if (value is double && value == 0.0) { - // Normalize -0.0 to 0.0 so they have the same hash code. - return 0.0.hashCode; - } - return value.hashCode; -} - -enum PlatformAssetPlaybackStyle { unknown, image, video, imageAnimated, livePhoto, videoLooping } - -class PlatformAsset { - PlatformAsset({ - required this.id, - required this.name, - required this.type, - this.createdAt, - this.updatedAt, - this.width, - this.height, - required this.durationMs, - required this.orientation, - required this.isFavorite, - this.adjustmentTime, - this.latitude, - this.longitude, - required this.playbackStyle, - }); - - String id; - - String name; - - int type; - - int? createdAt; - - int? updatedAt; - - int? width; - - int? height; - - int durationMs; - - int orientation; - - bool isFavorite; - - int? adjustmentTime; - - double? latitude; - - double? longitude; - - PlatformAssetPlaybackStyle playbackStyle; - - List _toList() { - return [ - id, - name, - type, - createdAt, - updatedAt, - width, - height, - durationMs, - orientation, - isFavorite, - adjustmentTime, - latitude, - longitude, - playbackStyle, - ]; - } - - Object encode() { - return _toList(); - } - - static PlatformAsset decode(Object result) { - result as List; - return PlatformAsset( - id: result[0]! as String, - name: result[1]! as String, - type: result[2]! as int, - createdAt: result[3] as int?, - updatedAt: result[4] as int?, - width: result[5] as int?, - height: result[6] as int?, - durationMs: result[7]! as int, - orientation: result[8]! as int, - isFavorite: result[9]! as bool, - adjustmentTime: result[10] as int?, - latitude: result[11] as double?, - longitude: result[12] as double?, - playbackStyle: result[13]! as PlatformAssetPlaybackStyle, - ); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - bool operator ==(Object other) { - if (other is! PlatformAsset || other.runtimeType != runtimeType) { - return false; - } - if (identical(this, other)) { - return true; - } - return _deepEquals(id, other.id) && - _deepEquals(name, other.name) && - _deepEquals(type, other.type) && - _deepEquals(createdAt, other.createdAt) && - _deepEquals(updatedAt, other.updatedAt) && - _deepEquals(width, other.width) && - _deepEquals(height, other.height) && - _deepEquals(durationMs, other.durationMs) && - _deepEquals(orientation, other.orientation) && - _deepEquals(isFavorite, other.isFavorite) && - _deepEquals(adjustmentTime, other.adjustmentTime) && - _deepEquals(latitude, other.latitude) && - _deepEquals(longitude, other.longitude) && - _deepEquals(playbackStyle, other.playbackStyle); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => _deepHash([runtimeType, ..._toList()]); -} - -class PlatformAlbum { - PlatformAlbum({ - required this.id, - required this.name, - this.updatedAt, - required this.isCloud, - required this.assetCount, - }); - - String id; - - String name; - - int? updatedAt; - - bool isCloud; - - int assetCount; - - List _toList() { - return [id, name, updatedAt, isCloud, assetCount]; - } - - Object encode() { - return _toList(); - } - - static PlatformAlbum decode(Object result) { - result as List; - return PlatformAlbum( - id: result[0]! as String, - name: result[1]! as String, - updatedAt: result[2] as int?, - isCloud: result[3]! as bool, - assetCount: result[4]! as int, - ); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - bool operator ==(Object other) { - if (other is! PlatformAlbum || other.runtimeType != runtimeType) { - return false; - } - if (identical(this, other)) { - return true; - } - return _deepEquals(id, other.id) && - _deepEquals(name, other.name) && - _deepEquals(updatedAt, other.updatedAt) && - _deepEquals(isCloud, other.isCloud) && - _deepEquals(assetCount, other.assetCount); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => _deepHash([runtimeType, ..._toList()]); -} - -class SyncDelta { - SyncDelta({required this.hasChanges, required this.updates, required this.deletes, required this.assetAlbums}); - - bool hasChanges; - - List updates; - - List deletes; - - Map> assetAlbums; - - List _toList() { - return [hasChanges, updates, deletes, assetAlbums]; - } - - Object encode() { - return _toList(); - } - - static SyncDelta decode(Object result) { - result as List; - return SyncDelta( - hasChanges: result[0]! as bool, - updates: (result[1]! as List).cast(), - deletes: (result[2]! as List).cast(), - assetAlbums: (result[3]! as Map).cast>(), - ); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - bool operator ==(Object other) { - if (other is! SyncDelta || other.runtimeType != runtimeType) { - return false; - } - if (identical(this, other)) { - return true; - } - return _deepEquals(hasChanges, other.hasChanges) && - _deepEquals(updates, other.updates) && - _deepEquals(deletes, other.deletes) && - _deepEquals(assetAlbums, other.assetAlbums); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => _deepHash([runtimeType, ..._toList()]); -} - -class HashResult { - HashResult({required this.assetId, this.error, this.hash}); - - String assetId; - - String? error; - - String? hash; - - List _toList() { - return [assetId, error, hash]; - } - - Object encode() { - return _toList(); - } - - static HashResult decode(Object result) { - result as List; - return HashResult(assetId: result[0]! as String, error: result[1] as String?, hash: result[2] as String?); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - bool operator ==(Object other) { - if (other is! HashResult || other.runtimeType != runtimeType) { - return false; - } - if (identical(this, other)) { - return true; - } - return _deepEquals(assetId, other.assetId) && _deepEquals(error, other.error) && _deepEquals(hash, other.hash); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => _deepHash([runtimeType, ..._toList()]); -} - -class CloudIdResult { - CloudIdResult({required this.assetId, this.error, this.cloudId}); - - String assetId; - - String? error; - - String? cloudId; - - List _toList() { - return [assetId, error, cloudId]; - } - - Object encode() { - return _toList(); - } - - static CloudIdResult decode(Object result) { - result as List; - return CloudIdResult(assetId: result[0]! as String, error: result[1] as String?, cloudId: result[2] as String?); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - bool operator ==(Object other) { - if (other is! CloudIdResult || other.runtimeType != runtimeType) { - return false; - } - if (identical(this, other)) { - return true; - } - return _deepEquals(assetId, other.assetId) && - _deepEquals(error, other.error) && - _deepEquals(cloudId, other.cloudId); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => _deepHash([runtimeType, ..._toList()]); -} - -class _PigeonCodec extends StandardMessageCodec { - const _PigeonCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is int) { - buffer.putUint8(4); - buffer.putInt64(value); - } else if (value is PlatformAssetPlaybackStyle) { - buffer.putUint8(129); - writeValue(buffer, value.index); - } else if (value is PlatformAsset) { - buffer.putUint8(130); - writeValue(buffer, value.encode()); - } else if (value is PlatformAlbum) { - buffer.putUint8(131); - writeValue(buffer, value.encode()); - } else if (value is SyncDelta) { - buffer.putUint8(132); - writeValue(buffer, value.encode()); - } else if (value is HashResult) { - buffer.putUint8(133); - writeValue(buffer, value.encode()); - } else if (value is CloudIdResult) { - buffer.putUint8(134); - writeValue(buffer, value.encode()); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 129: - final value = readValue(buffer) as int?; - return value == null ? null : PlatformAssetPlaybackStyle.values[value]; - case 130: - return PlatformAsset.decode(readValue(buffer)!); - case 131: - return PlatformAlbum.decode(readValue(buffer)!); - case 132: - return SyncDelta.decode(readValue(buffer)!); - case 133: - return HashResult.decode(readValue(buffer)!); - case 134: - return CloudIdResult.decode(readValue(buffer)!); - default: - return super.readValueOfType(type, buffer); - } - } -} - -class NativeSyncApi { - /// Constructor for [NativeSyncApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - NativeSyncApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; - - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - final String pigeonVar_messageChannelSuffix; - - Future shouldFullSync() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.shouldFullSync$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as bool; - } - - Future getMediaChanges() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getMediaChanges$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as SyncDelta; - } - - Future checkpointSync() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.checkpointSync$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future clearSyncCheckpoint() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.clearSyncCheckpoint$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future> getAssetIdsForAlbum(String albumId) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetIdsForAlbum$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([albumId]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return (pigeonVar_replyValue! as List).cast(); - } - - Future> getAlbums() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAlbums$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return (pigeonVar_replyValue! as List).cast(); - } - - Future getAssetsCountSince(String albumId, int timestamp) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsCountSince$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([albumId, timestamp]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as int; - } - - Future> getAssetsForAlbum(String albumId, {int? updatedTimeCond}) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getAssetsForAlbum$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([albumId, updatedTimeCond]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return (pigeonVar_replyValue! as List).cast(); - } - - Future> hashAssets(List assetIds, {bool allowNetworkAccess = false}) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.hashAssets$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([assetIds, allowNetworkAccess]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return (pigeonVar_replyValue! as List).cast(); - } - - Future cancelHashing() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.cancelHashing$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future cancelSync() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.cancelSync$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future>> getTrashedAssets() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getTrashedAssets$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return (pigeonVar_replyValue! as Map).cast>(); - } - - Future restoreFromTrashById(String mediaId, int type) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.restoreFromTrashById$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([mediaId, type]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as bool; - } - - Future> getCloudIdForAssetIds(List assetIds) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([assetIds]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return (pigeonVar_replyValue! as List).cast(); - } -} diff --git a/mobile/lib/platform/network_api.g.dart b/mobile/lib/platform/network_api.g.dart deleted file mode 100644 index 6258060bfb..0000000000 --- a/mobile/lib/platform/network_api.g.dart +++ /dev/null @@ -1,331 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: unused_import, unused_shown_name -// ignore_for_file: type=lint - -import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List; - -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; - -Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); - } else if (replyList.length > 1) { - throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); - } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } - return replyList.firstOrNull; -} - -bool _deepEquals(Object? a, Object? b) { - if (identical(a, b)) { - return true; - } - if (a is double && b is double) { - if (a.isNaN && b.isNaN) { - return true; - } - return a == b; - } - if (a is List && b is List) { - return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); - } - if (a is Map && b is Map) { - if (a.length != b.length) { - return false; - } - for (final MapEntry entryA in a.entries) { - bool found = false; - for (final MapEntry entryB in b.entries) { - if (_deepEquals(entryA.key, entryB.key)) { - if (_deepEquals(entryA.value, entryB.value)) { - found = true; - break; - } else { - return false; - } - } - } - if (!found) { - return false; - } - } - return true; - } - return a == b; -} - -int _deepHash(Object? value) { - if (value is List) { - return Object.hashAll(value.map(_deepHash)); - } - if (value is Map) { - int result = 0; - for (final MapEntry entry in value.entries) { - result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value); - } - return result; - } - if (value is double && value.isNaN) { - // Normalize NaN to a consistent hash. - return 0x7FF8000000000000.hashCode; - } - if (value is double && value == 0.0) { - // Normalize -0.0 to 0.0 so they have the same hash code. - return 0.0.hashCode; - } - return value.hashCode; -} - -class ClientCertData { - ClientCertData({required this.data, required this.password}); - - Uint8List data; - - String password; - - List _toList() { - return [data, password]; - } - - Object encode() { - return _toList(); - } - - static ClientCertData decode(Object result) { - result as List; - return ClientCertData(data: result[0]! as Uint8List, password: result[1]! as String); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - bool operator ==(Object other) { - if (other is! ClientCertData || other.runtimeType != runtimeType) { - return false; - } - if (identical(this, other)) { - return true; - } - return _deepEquals(data, other.data) && _deepEquals(password, other.password); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => _deepHash([runtimeType, ..._toList()]); -} - -class ClientCertPrompt { - ClientCertPrompt({required this.title, required this.message, required this.cancel, required this.confirm}); - - String title; - - String message; - - String cancel; - - String confirm; - - List _toList() { - return [title, message, cancel, confirm]; - } - - Object encode() { - return _toList(); - } - - static ClientCertPrompt decode(Object result) { - result as List; - return ClientCertPrompt( - title: result[0]! as String, - message: result[1]! as String, - cancel: result[2]! as String, - confirm: result[3]! as String, - ); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - bool operator ==(Object other) { - if (other is! ClientCertPrompt || other.runtimeType != runtimeType) { - return false; - } - if (identical(this, other)) { - return true; - } - return _deepEquals(title, other.title) && - _deepEquals(message, other.message) && - _deepEquals(cancel, other.cancel) && - _deepEquals(confirm, other.confirm); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => _deepHash([runtimeType, ..._toList()]); -} - -class _PigeonCodec extends StandardMessageCodec { - const _PigeonCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is int) { - buffer.putUint8(4); - buffer.putInt64(value); - } else if (value is ClientCertData) { - buffer.putUint8(129); - writeValue(buffer, value.encode()); - } else if (value is ClientCertPrompt) { - buffer.putUint8(130); - writeValue(buffer, value.encode()); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 129: - return ClientCertData.decode(readValue(buffer)!); - case 130: - return ClientCertPrompt.decode(readValue(buffer)!); - default: - return super.readValueOfType(type, buffer); - } - } -} - -class NetworkApi { - /// Constructor for [NetworkApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - NetworkApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; - - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - final String pigeonVar_messageChannelSuffix; - - Future addCertificate(ClientCertData clientData) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NetworkApi.addCertificate$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([clientData]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future selectCertificate(ClientCertPrompt promptText) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NetworkApi.selectCertificate$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([promptText]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future removeCertificate() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NetworkApi.removeCertificate$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future hasCertificate() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NetworkApi.hasCertificate$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as bool; - } - - Future getClientPointer() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NetworkApi.getClientPointer$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as int; - } - - Future setRequestHeaders(Map headers, List serverUrls, String? token) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NetworkApi.setRequestHeaders$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([headers, serverUrls, token]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future getAppGroupId() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.NetworkApi.getAppGroupId$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as String; - } -} diff --git a/mobile/lib/platform/permission_api.g.dart b/mobile/lib/platform/permission_api.g.dart deleted file mode 100644 index 7b85d611d2..0000000000 --- a/mobile/lib/platform/permission_api.g.dart +++ /dev/null @@ -1,146 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: unused_import, unused_shown_name -// ignore_for_file: type=lint - -import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List; - -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; - -Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); - } else if (replyList.length > 1) { - throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); - } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } - return replyList.firstOrNull; -} - -enum PermissionStatus { granted, denied, permanentlyDenied } - -class _PigeonCodec extends StandardMessageCodec { - const _PigeonCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is int) { - buffer.putUint8(4); - buffer.putInt64(value); - } else if (value is PermissionStatus) { - buffer.putUint8(129); - writeValue(buffer, value.index); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 129: - final value = readValue(buffer) as int?; - return value == null ? null : PermissionStatus.values[value]; - default: - return super.readValueOfType(type, buffer); - } - } -} - -class PermissionApi { - /// Constructor for [PermissionApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - PermissionApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; - - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - final String pigeonVar_messageChannelSuffix; - - Future isIgnoringBatteryOptimizations() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.PermissionApi.isIgnoringBatteryOptimizations$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as PermissionStatus; - } - - Future hasManageMediaPermission() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as bool; - } - - Future requestManageMediaPermission() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.PermissionApi.requestManageMediaPermission$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as bool; - } - - Future manageMediaPermission() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.PermissionApi.manageMediaPermission$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as bool; - } -} diff --git a/mobile/lib/platform/remote_image_api.g.dart b/mobile/lib/platform/remote_image_api.g.dart deleted file mode 100644 index 5239cb3e45..0000000000 --- a/mobile/lib/platform/remote_image_api.g.dart +++ /dev/null @@ -1,114 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: unused_import, unused_shown_name -// ignore_for_file: type=lint - -import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List; - -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; - -Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); - } else if (replyList.length > 1) { - throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); - } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } - return replyList.firstOrNull; -} - -class _PigeonCodec extends StandardMessageCodec { - const _PigeonCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is int) { - buffer.putUint8(4); - buffer.putInt64(value); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - default: - return super.readValueOfType(type, buffer); - } - } -} - -class RemoteImageApi { - /// Constructor for [RemoteImageApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - RemoteImageApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; - - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - final String pigeonVar_messageChannelSuffix; - - Future?> requestImage(String url, {required int requestId, required bool preferEncoded}) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.RemoteImageApi.requestImage$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([url, requestId, preferEncoded]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ); - return (pigeonVar_replyValue as Map?)?.cast(); - } - - Future cancelRequest(int requestId) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.RemoteImageApi.cancelRequest$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([requestId]); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); - } - - Future clearCache() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.RemoteImageApi.clearCache$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ); - return pigeonVar_replyValue! as int; - } -} diff --git a/mobile/lib/platform/thumbnail_api.g.dart b/mobile/lib/platform/thumbnail_api.g.dart deleted file mode 100644 index 53d7b10fc3..0000000000 --- a/mobile/lib/platform/thumbnail_api.g.dart +++ /dev/null @@ -1,142 +0,0 @@ -// Autogenerated from Pigeon (v26.0.2), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers - -import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; - -import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; -import 'package:flutter/services.dart'; - -PlatformException _createConnectionError(String channelName) { - return PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); -} - -class _PigeonCodec extends StandardMessageCodec { - const _PigeonCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is int) { - buffer.putUint8(4); - buffer.putInt64(value); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - default: - return super.readValueOfType(type, buffer); - } - } -} - -class ThumbnailApi { - /// Constructor for [ThumbnailApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - ThumbnailApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; - - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - final String pigeonVar_messageChannelSuffix; - - Future> requestImage( - String assetId, { - required int requestId, - required int width, - required int height, - required bool isVideo, - }) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.ThumbnailApi.requestImage$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([ - assetId, - requestId, - width, - height, - isVideo, - ]); - final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as Map?)!.cast(); - } - } - - Future cancelImageRequest(int requestId) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.ThumbnailApi.cancelImageRequest$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([requestId]); - final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } - } - - Future> getThumbhash(String thumbhash) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.ThumbnailApi.getThumbhash$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([thumbhash]); - final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as Map?)!.cast(); - } - } -} diff --git a/mobile/lib/platform/view_intent_api.g.dart b/mobile/lib/platform/view_intent_api.g.dart deleted file mode 100644 index d457c249de..0000000000 --- a/mobile/lib/platform/view_intent_api.g.dart +++ /dev/null @@ -1,191 +0,0 @@ -// Autogenerated from Pigeon (v26.3.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: unused_import, unused_shown_name -// ignore_for_file: type=lint - -import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List; - -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; - -Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); - } else if (replyList.length > 1) { - throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); - } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } - return replyList.firstOrNull; -} - -bool _deepEquals(Object? a, Object? b) { - if (identical(a, b)) { - return true; - } - if (a is double && b is double) { - if (a.isNaN && b.isNaN) { - return true; - } - return a == b; - } - if (a is List && b is List) { - return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); - } - if (a is Map && b is Map) { - if (a.length != b.length) { - return false; - } - for (final MapEntry entryA in a.entries) { - bool found = false; - for (final MapEntry entryB in b.entries) { - if (_deepEquals(entryA.key, entryB.key)) { - if (_deepEquals(entryA.value, entryB.value)) { - found = true; - break; - } else { - return false; - } - } - } - if (!found) { - return false; - } - } - return true; - } - return a == b; -} - -int _deepHash(Object? value) { - if (value is List) { - return Object.hashAll(value.map(_deepHash)); - } - if (value is Map) { - int result = 0; - for (final MapEntry entry in value.entries) { - result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value); - } - return result; - } - if (value is double && value.isNaN) { - // Normalize NaN to a consistent hash. - return 0x7FF8000000000000.hashCode; - } - if (value is double && value == 0.0) { - // Normalize -0.0 to 0.0 so they have the same hash code. - return 0.0.hashCode; - } - return value.hashCode; -} - -class ViewIntentPayload { - ViewIntentPayload({this.path, required this.mimeType, this.localAssetId}); - - String? path; - - String mimeType; - - String? localAssetId; - - List _toList() { - return [path, mimeType, localAssetId]; - } - - Object encode() { - return _toList(); - } - - static ViewIntentPayload decode(Object result) { - result as List; - return ViewIntentPayload( - path: result[0] as String?, - mimeType: result[1]! as String, - localAssetId: result[2] as String?, - ); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - bool operator ==(Object other) { - if (other is! ViewIntentPayload || other.runtimeType != runtimeType) { - return false; - } - if (identical(this, other)) { - return true; - } - return _deepEquals(path, other.path) && - _deepEquals(mimeType, other.mimeType) && - _deepEquals(localAssetId, other.localAssetId); - } - - @override - // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => _deepHash([runtimeType, ..._toList()]); -} - -class _PigeonCodec extends StandardMessageCodec { - const _PigeonCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is int) { - buffer.putUint8(4); - buffer.putInt64(value); - } else if (value is ViewIntentPayload) { - buffer.putUint8(129); - writeValue(buffer, value.encode()); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 129: - return ViewIntentPayload.decode(readValue(buffer)!); - default: - return super.readValueOfType(type, buffer); - } - } -} - -class ViewIntentHostApi { - /// Constructor for [ViewIntentHostApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - ViewIntentHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; - - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - final String pigeonVar_messageChannelSuffix; - - Future consumeViewIntent() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.ViewIntentHostApi.consumeViewIntent$pigeonVar_messageChannelSuffix'; - final pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - - final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ); - return pigeonVar_replyValue as ViewIntentPayload?; - } -} From 858aeadce8097ff66bee9a53ebe03d982db8b0ba Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 30 Jul 2026 12:15:35 -0700 Subject: [PATCH 11/81] chore(mobile): Apply stricter linting rules for Flutter and known issues (#30373) --- mobile/analysis_options.yaml | 13 ++++++++++++- mobile/bin/generate_keys.dart | 2 +- .../repositories/storage.repository.dart | 2 ++ .../backup/drift_backup_album_selection.page.dart | 2 +- .../lib/pages/backup/drift_upload_detail.page.dart | 2 +- mobile/lib/pages/common/app_log_detail.page.dart | 4 ++-- mobile/lib/pages/common/large_leading_tile.dart | 2 +- .../presentation/pages/drift_activities.page.dart | 2 +- .../lib/presentation/pages/drift_library.page.dart | 2 +- .../lib/presentation/pages/drift_memory.page.dart | 2 +- .../pages/drift_partner_detail.page.dart | 2 +- .../presentation/pages/drift_slideshow.page.dart | 2 +- .../pages/profile/profile_picture_crop.page.dart | 2 +- .../pages/search/drift_search.page.dart | 4 ++-- .../widgets/album/album_selector.widget.dart | 4 ++-- .../lib/presentation/widgets/album/album_tile.dart | 2 +- .../widgets/asset_viewer/video_viewer.widget.dart | 1 + .../images/local_album_thumbnail.widget.dart | 2 +- .../widgets/images/thumbnail_tile.widget.dart | 2 +- .../widgets/memory/memory_card.widget.dart | 4 ++-- mobile/lib/services/download.service.dart | 2 ++ mobile/lib/services/foreground_upload.service.dart | 1 + mobile/lib/services/view_intent.service.dart | 1 + mobile/lib/widgets/common/tag_picker.dart | 4 ++-- .../widgets/map/map_settings/map_theme_picker.dart | 2 +- .../beta_sync_settings/sync_status_and_actions.dart | 2 ++ .../widgets/settings/free_up_space_settings.dart | 2 +- .../preference_settings/primary_color_setting.dart | 2 +- mobile/test/services/view_intent_service_test.dart | 8 +++----- 29 files changed, 50 insertions(+), 32 deletions(-) diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 1a7b463913..b9f18d0d81 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -29,7 +29,6 @@ linter: # Formatting avoid_print: true unawaited_futures: true - use_build_context_synchronously: false require_trailing_commas: true unrelated_type_equality_checks: true prefer_const_constructors: true @@ -51,6 +50,18 @@ linter: avoid_multiple_declarations_per_line: true unnecessary_breaks: true + # Known issues + avoid_slow_async_io: true + avoid_type_to_string: true + + # Flutter specific + use_build_context_synchronously: false + sized_box_for_whitespace: true + use_colored_box: true + use_decorated_box: true + avoid_unnecessary_containers: true + use_full_hex_values_for_flutter_colors: true + # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options analyzer: diff --git a/mobile/bin/generate_keys.dart b/mobile/bin/generate_keys.dart index a4cf562bcb..1ce643be12 100644 --- a/mobile/bin/generate_keys.dart +++ b/mobile/bin/generate_keys.dart @@ -1,4 +1,4 @@ -// ignore_for_file: avoid_print +// ignore_for_file: avoid_slow_async_io, avoid_print import 'dart:convert'; import 'dart:io'; diff --git a/mobile/lib/infrastructure/repositories/storage.repository.dart b/mobile/lib/infrastructure/repositories/storage.repository.dart index 3a63812485..9500190abc 100644 --- a/mobile/lib/infrastructure/repositories/storage.repository.dart +++ b/mobile/lib/infrastructure/repositories/storage.repository.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_slow_async_io + import 'dart:io'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; diff --git a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart index 9f60a4e193..6589741aab 100644 --- a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart @@ -286,7 +286,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState { SizedBox( width: 48, height: 48, - child: Container( + child: DecoratedBox( decoration: BoxDecoration( color: context.colorScheme.outline.withValues(alpha: 0.1), borderRadius: const BorderRadius.all(Radius.circular(8)), diff --git a/mobile/lib/pages/common/app_log_detail.page.dart b/mobile/lib/pages/common/app_log_detail.page.dart index 274231a729..ab7668f845 100644 --- a/mobile/lib/pages/common/app_log_detail.page.dart +++ b/mobile/lib/pages/common/app_log_detail.page.dart @@ -48,7 +48,7 @@ class AppLogDetailPage extends HookConsumerWidget { ), ], ), - Container( + DecoratedBox( decoration: BoxDecoration( color: context.colorScheme.surfaceContainerHigh, borderRadius: const BorderRadius.all(Radius.circular(15.0)), @@ -79,7 +79,7 @@ class AppLogDetailPage extends HookConsumerWidget { style: TextStyle(fontSize: 12.0, color: context.primaryColor, fontWeight: FontWeight.bold), ), ), - Container( + DecoratedBox( decoration: BoxDecoration( color: context.colorScheme.surfaceContainerHigh, borderRadius: const BorderRadius.all(Radius.circular(15.0)), diff --git a/mobile/lib/pages/common/large_leading_tile.dart b/mobile/lib/pages/common/large_leading_tile.dart index 4563834473..58528008ca 100644 --- a/mobile/lib/pages/common/large_leading_tile.dart +++ b/mobile/lib/pages/common/large_leading_tile.dart @@ -34,7 +34,7 @@ class LargeLeadingTile extends StatelessWidget { return InkWell( borderRadius: BorderRadius.circular(borderRadius), onTap: disabled ? null : onTap, - child: Container( + child: DecoratedBox( decoration: BoxDecoration( color: selected ? selectedTileColor ?? Theme.of(context).primaryColor.withAlpha(30) diff --git a/mobile/lib/presentation/pages/drift_activities.page.dart b/mobile/lib/presentation/pages/drift_activities.page.dart index a52f1d7358..59c9a8a1e1 100644 --- a/mobile/lib/presentation/pages/drift_activities.page.dart +++ b/mobile/lib/presentation/pages/drift_activities.page.dart @@ -71,7 +71,7 @@ class DriftActivitiesPage extends HookConsumerWidget { ), Align( alignment: Alignment.bottomCenter, - child: Container( + child: DecoratedBox( decoration: BoxDecoration( color: context.scaffoldBackgroundColor, border: Border(top: BorderSide(color: context.colorScheme.secondaryContainer, width: 1)), diff --git a/mobile/lib/presentation/pages/drift_library.page.dart b/mobile/lib/presentation/pages/drift_library.page.dart index 190ad3af6a..b2b4d250f1 100644 --- a/mobile/lib/presentation/pages/drift_library.page.dart +++ b/mobile/lib/presentation/pages/drift_library.page.dart @@ -354,7 +354,7 @@ class _QuickAccessButtonList extends ConsumerWidget { return SliverPadding( padding: const EdgeInsets.only(left: 16, top: 12, right: 16, bottom: 32), sliver: SliverToBoxAdapter( - child: Container( + child: DecoratedBox( decoration: BoxDecoration( border: Border.all(color: context.colorScheme.onSurface.withAlpha(10), width: 1), borderRadius: const BorderRadius.all(Radius.circular(20)), diff --git a/mobile/lib/presentation/pages/drift_memory.page.dart b/mobile/lib/presentation/pages/drift_memory.page.dart index 4ae97f30e1..b8f3c94a00 100644 --- a/mobile/lib/presentation/pages/drift_memory.page.dart +++ b/mobile/lib/presentation/pages/drift_memory.page.dart @@ -281,7 +281,7 @@ class DriftMemoryPage extends HookConsumerWidget { final asset = memories[mIndex].assets[index]; return Stack( children: [ - Container( + ColoredBox( color: Colors.black, child: DriftMemoryCard( asset: asset, diff --git a/mobile/lib/presentation/pages/drift_partner_detail.page.dart b/mobile/lib/presentation/pages/drift_partner_detail.page.dart index fd5b64c108..53353ce689 100644 --- a/mobile/lib/presentation/pages/drift_partner_detail.page.dart +++ b/mobile/lib/presentation/pages/drift_partner_detail.page.dart @@ -89,7 +89,7 @@ class _InfoBoxState extends ConsumerState<_InfoBox> { height: 110, child: Padding( padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 16.0), - child: Container( + child: DecoratedBox( decoration: BoxDecoration( border: Border.all(color: context.colorScheme.onSurface.withAlpha(10), width: 1), borderRadius: const BorderRadius.all(Radius.circular(20)), diff --git a/mobile/lib/presentation/pages/drift_slideshow.page.dart b/mobile/lib/presentation/pages/drift_slideshow.page.dart index 3f0c441c01..9b7c10c891 100644 --- a/mobile/lib/presentation/pages/drift_slideshow.page.dart +++ b/mobile/lib/presentation/pages/drift_slideshow.page.dart @@ -313,7 +313,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si return ImageFiltered( imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), - child: Container( + child: DecoratedBox( decoration: BoxDecoration( image: DecorationImage( image: getFullImageProvider(asset, size: Size(context.width, context.height)), diff --git a/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart b/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart index 3fb32b7d93..e6ae6ad44f 100644 --- a/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart +++ b/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart @@ -157,7 +157,7 @@ class _ProfilePictureCropPageState extends ConsumerState return Center( child: ConstrainedBox( constraints: BoxConstraints(maxHeight: context.height * 0.7, maxWidth: context.width * 0.9), - child: Container( + child: DecoratedBox( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(7)), boxShadow: [ diff --git a/mobile/lib/presentation/pages/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/drift_search.page.dart index 6b818bd273..4d04967b28 100644 --- a/mobile/lib/presentation/pages/search/drift_search.page.dart +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -598,7 +598,7 @@ class DriftSearchPage extends HookConsumerWidget { ), ), ], - title: Container( + title: DecoratedBox( decoration: BoxDecoration( border: Border.all(color: context.colorScheme.onSurface.withAlpha(0), width: 0), borderRadius: const BorderRadius.all(Radius.circular(24)), @@ -859,7 +859,7 @@ class _QuickLinkList extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( + return DecoratedBox( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(20)), border: Border.all(color: context.colorScheme.outline.withAlpha(10), width: 1), diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index 285c6290a9..ba6c6da560 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -395,7 +395,7 @@ class _SearchBar extends StatelessWidget { return SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), sliver: SliverToBoxAdapter( - child: Container( + child: DecoratedBox( decoration: BoxDecoration( border: Border.all(color: context.colorScheme.onSurface.withAlpha(0), width: 0), borderRadius: const BorderRadius.all(Radius.circular(24)), @@ -699,7 +699,7 @@ class _GridAlbumCard extends ConsumerWidget { ); } - return Container( + return ColoredBox( color: context.colorScheme.surfaceContainerHighest, child: const Icon(Icons.photo_album_rounded, size: 40, color: Colors.grey), ); diff --git a/mobile/lib/presentation/widgets/album/album_tile.dart b/mobile/lib/presentation/widgets/album/album_tile.dart index 1aeadf61bc..bbf7e11e5a 100644 --- a/mobile/lib/presentation/widgets/album/album_tile.dart +++ b/mobile/lib/presentation/widgets/album/album_tile.dart @@ -51,7 +51,7 @@ class AlbumTile extends ConsumerWidget { : SizedBox( width: 80, height: 80, - child: Container( + child: DecoratedBox( decoration: BoxDecoration( color: context.colorScheme.surfaceContainer, borderRadius: const BorderRadius.all(Radius.circular(16)), diff --git a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart index d007883ec9..6f2398046f 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -112,6 +112,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg final localFilePath = widget.localFilePath; if (localFilePath != null) { final file = File(localFilePath); + // ignore: avoid_slow_async_io if (!await file.exists()) { throw Exception('No file found for the video'); } diff --git a/mobile/lib/presentation/widgets/images/local_album_thumbnail.widget.dart b/mobile/lib/presentation/widgets/images/local_album_thumbnail.widget.dart index b519da33c3..966e070872 100644 --- a/mobile/lib/presentation/widgets/images/local_album_thumbnail.widget.dart +++ b/mobile/lib/presentation/widgets/images/local_album_thumbnail.widget.dart @@ -14,7 +14,7 @@ class LocalAlbumThumbnail extends ConsumerWidget { return localAlbumThumbnail.when( data: (data) { if (data == null) { - return Container( + return DecoratedBox( decoration: BoxDecoration( color: context.colorScheme.surfaceContainer, borderRadius: const BorderRadius.all(Radius.circular(16)), diff --git a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart index 7d71f0296d..1d3dcb2cf0 100644 --- a/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart +++ b/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart @@ -346,7 +346,7 @@ class _UploadProgressOverlay extends StatelessWidget { final percentage = isError ? 0 : (progress * 100).toInt(); return Positioned.fill( - child: Container( + child: ColoredBox( color: isError ? Colors.red.withValues(alpha: 0.6) : Colors.black54, child: Center( child: Column( diff --git a/mobile/lib/presentation/widgets/memory/memory_card.widget.dart b/mobile/lib/presentation/widgets/memory/memory_card.widget.dart index 2a88de8e0a..7e782a3db4 100644 --- a/mobile/lib/presentation/widgets/memory/memory_card.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_card.widget.dart @@ -97,7 +97,7 @@ class _BlurredBackdrop extends HookWidget { final blurhash = useDriftBlurHashRef(asset).value; if (blurhash != null) { // Use a nice cheap blur hash image decoration - return Container( + return DecoratedBox( decoration: BoxDecoration( image: DecorationImage(image: MemoryImage(blurhash), fit: BoxFit.cover), ), @@ -109,7 +109,7 @@ class _BlurredBackdrop extends HookWidget { // safely use that as the image provider return ImageFiltered( imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), - child: Container( + child: DecoratedBox( decoration: BoxDecoration( image: DecorationImage( image: getFullImageProvider(asset, size: Size(context.width, context.height)), diff --git a/mobile/lib/services/download.service.dart b/mobile/lib/services/download.service.dart index de8e8af3f5..f38b20cc21 100644 --- a/mobile/lib/services/download.service.dart +++ b/mobile/lib/services/download.service.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_slow_async_io + import 'dart:async'; import 'dart:io'; diff --git a/mobile/lib/services/foreground_upload.service.dart b/mobile/lib/services/foreground_upload.service.dart index 7c0352a00e..36d3975a26 100644 --- a/mobile/lib/services/foreground_upload.service.dart +++ b/mobile/lib/services/foreground_upload.service.dart @@ -419,6 +419,7 @@ class ForegroundUploadService { void Function(int bytes, int totalBytes)? onProgress, }) async { try { + // ignore: avoid_slow_async_io final stats = await file.stat(); final fileCreatedAt = stats.changed; final fileModifiedAt = stats.modified; diff --git a/mobile/lib/services/view_intent.service.dart b/mobile/lib/services/view_intent.service.dart index 22a3407e5a..e822d1ebb6 100644 --- a/mobile/lib/services/view_intent.service.dart +++ b/mobile/lib/services/view_intent.service.dart @@ -61,6 +61,7 @@ class ViewIntentService { try { final file = File(path); + // ignore: avoid_slow_async_io if (await file.exists()) { await file.delete(); } diff --git a/mobile/lib/widgets/common/tag_picker.dart b/mobile/lib/widgets/common/tag_picker.dart index 97fbff1930..a9a68fe044 100644 --- a/mobile/lib/widgets/common/tag_picker.dart +++ b/mobile/lib/widgets/common/tag_picker.dart @@ -125,7 +125,7 @@ class TagPicker extends HookConsumerWidget { // Create new tag tile return Padding( padding: const EdgeInsets.only(bottom: 2.0), - child: Container( + child: DecoratedBox( decoration: BoxDecoration( color: isCreateSelected ? context.primaryColor : context.primaryColor.withAlpha(25), borderRadius: const BorderRadius.all(Radius.circular(10)), @@ -160,7 +160,7 @@ class TagPicker extends HookConsumerWidget { return Padding( padding: const EdgeInsets.only(bottom: 2.0), - child: Container( + child: DecoratedBox( decoration: BoxDecoration( color: isSelected ? context.primaryColor : context.primaryColor.withAlpha(25), borderRadius: borderRadius, diff --git a/mobile/lib/widgets/map/map_settings/map_theme_picker.dart b/mobile/lib/widgets/map/map_settings/map_theme_picker.dart index 7866c0ecdc..e66f08c221 100644 --- a/mobile/lib/widgets/map/map_settings/map_theme_picker.dart +++ b/mobile/lib/widgets/map/map_settings/map_theme_picker.dart @@ -69,7 +69,7 @@ class _BorderedMapThumbnail extends StatelessWidget { Widget build(BuildContext context) { return Column( children: [ - Container( + DecoratedBox( decoration: BoxDecoration( border: Border.fromBorderSide( BorderSide(width: 4, color: shouldHighlight ? context.colorScheme.onSurface : Colors.transparent), diff --git a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart index 92787077a1..7bd604ae5e 100644 --- a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart +++ b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart @@ -39,6 +39,7 @@ class SyncStatusAndActions extends HookConsumerWidget { final documentsDir = await getApplicationDocumentsDirectory(); final dbFile = File(path.join(documentsDir.path, 'immich.sqlite')); + // ignore: avoid_slow_async_io if (!await dbFile.exists()) { if (context.mounted) { context.scaffoldMessenger.showSnackBar( @@ -61,6 +62,7 @@ class SyncStatusAndActions extends HookConsumerWidget { ); Future.delayed(const Duration(seconds: 30), () async { + // ignore: avoid_slow_async_io if (await exportFile.exists()) { await exportFile.delete(); } diff --git a/mobile/lib/widgets/settings/free_up_space_settings.dart b/mobile/lib/widgets/settings/free_up_space_settings.dart index 7b16c2d67d..dbec3a2dcb 100644 --- a/mobile/lib/widgets/settings/free_up_space_settings.dart +++ b/mobile/lib/widgets/settings/free_up_space_settings.dart @@ -773,7 +773,7 @@ class _DatePresetCard extends StatelessWidget { child: InkWell( onTap: onTap, borderRadius: const BorderRadius.all(Radius.circular(12)), - child: Container( + child: DecoratedBox( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(12)), border: Border.all(color: isSelected ? context.colorScheme.primary : Colors.transparent, width: 1), diff --git a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart index 3fead2c59f..1defd2df44 100644 --- a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart @@ -69,7 +69,7 @@ class PrimaryColorSetting extends HookConsumerWidget { right: 0, top: 0, bottom: 0, - child: Container( + child: DecoratedBox( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(100)), color: Colors.grey[900]?.withValues(alpha: .4), diff --git a/mobile/test/services/view_intent_service_test.dart b/mobile/test/services/view_intent_service_test.dart index 7b3d0b85e7..fd8f5f725c 100644 --- a/mobile/test/services/view_intent_service_test.dart +++ b/mobile/test/services/view_intent_service_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_slow_async_io + import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; @@ -13,11 +15,7 @@ void main() { late Directory tempRoot; late Directory cacheDir; - final attachment = ViewIntentPayload( - path: '/tmp/file.jpg', - mimeType: 'image/jpeg', - localAssetId: '42', - ); + final attachment = ViewIntentPayload(path: '/tmp/file.jpg', mimeType: 'image/jpeg', localAssetId: '42'); setUp(() { hostApi = MockViewIntentHostApi(); From 56fbca910eba70eeb53abe66e4ff205803371a95 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 30 Jul 2026 21:17:57 +0200 Subject: [PATCH 12/81] chore: skip e2e tests when the stack fails to start (#30414) --- .github/workflows/test.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b9fb652f59..a01b429713 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -441,6 +441,7 @@ jobs: if: ${{ !cancelled() }} - name: Start Docker Compose + id: docker run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300 if: ${{ !cancelled() }} @@ -448,13 +449,13 @@ jobs: env: VITEST_DISABLE_DOCKER_SETUP: true run: pnpm test - if: ${{ !cancelled() }} + if: ${{ !cancelled() && steps.docker.outcome == 'success' }} - name: Run e2e tests (maintenance) env: VITEST_DISABLE_DOCKER_SETUP: true run: pnpm test:maintenance - if: ${{ !cancelled() }} + if: ${{ !cancelled() && steps.docker.outcome == 'success' }} - name: Capture Docker logs if: always() @@ -519,6 +520,7 @@ jobs: if: ${{ !cancelled() }} - name: Docker build + id: docker run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300 if: ${{ !cancelled() }} @@ -526,7 +528,7 @@ jobs: env: PLAYWRIGHT_DISABLE_WEBSERVER: true run: pnpm test:web - if: ${{ !cancelled() }} + if: ${{ !cancelled() && steps.docker.outcome == 'success' }} - name: Archive e2e test (web) results uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -539,7 +541,7 @@ jobs: env: PLAYWRIGHT_DISABLE_WEBSERVER: true run: pnpm test:web:ui - if: ${{ !cancelled() }} + if: ${{ !cancelled() && steps.docker.outcome == 'success' }} - name: Archive ui test (web) results uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -552,7 +554,7 @@ jobs: env: PLAYWRIGHT_DISABLE_WEBSERVER: true run: pnpm test:web:maintenance - if: ${{ !cancelled() }} + if: ${{ !cancelled() && steps.docker.outcome == 'success' }} - name: Archive maintenance tests (web) results uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 From 9732bebb55ad594fe0d9ac5f13a1be7ddab109b9 Mon Sep 17 00:00:00 2001 From: Devesh Kolte Date: Fri, 31 Jul 2026 00:49:58 +0530 Subject: [PATCH 13/81] fix(server): store null instead of empty string for user password (#30223) --- ...86754473-ConvertUserPasswordEmptyStringToNull.ts | 13 +++++++++++++ server/src/schema/tables/user.table.ts | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 server/src/schema/migrations/1784986754473-ConvertUserPasswordEmptyStringToNull.ts diff --git a/server/src/schema/migrations/1784986754473-ConvertUserPasswordEmptyStringToNull.ts b/server/src/schema/migrations/1784986754473-ConvertUserPasswordEmptyStringToNull.ts new file mode 100644 index 0000000000..82f742c90a --- /dev/null +++ b/server/src/schema/migrations/1784986754473-ConvertUserPasswordEmptyStringToNull.ts @@ -0,0 +1,13 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "user" ALTER COLUMN "password" DROP NOT NULL;`.execute(db); + await sql`ALTER TABLE "user" ALTER COLUMN "password" SET DEFAULT NULL;`.execute(db); + await sql`UPDATE "user" SET "password" = NULL WHERE "password" = '';`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`UPDATE "user" SET "password" = '' WHERE "password" IS NULL;`.execute(db); + await sql`ALTER TABLE "user" ALTER COLUMN "password" SET DEFAULT '';`.execute(db); + await sql`ALTER TABLE "user" ALTER COLUMN "password" SET NOT NULL;`.execute(db); +} diff --git a/server/src/schema/tables/user.table.ts b/server/src/schema/tables/user.table.ts index 0839924d2a..50d56d9067 100644 --- a/server/src/schema/tables/user.table.ts +++ b/server/src/schema/tables/user.table.ts @@ -31,8 +31,8 @@ export class UserTable { @Column({ unique: true }) email!: string; - @Column({ default: '' }) - password!: Generated; + @Column({ nullable: true, default: null }) + password!: string | null; @Column({ nullable: true }) pinCode!: string | null; From 7149dd80307ffa92be49103bcdcf1485d75db1b7 Mon Sep 17 00:00:00 2001 From: Matthew Momjian <50788000+mmomjian@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:34:08 -0400 Subject: [PATCH 14/81] fix(docs): remove listing unraid as an "official" deployment (#30323) * remove listing unraid as an "official" deploymeny * mplconfig * oops --- docs/docs/install/unraid.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/docs/install/unraid.md b/docs/docs/install/unraid.md index 384d3d11d7..3ce98dc2e1 100644 --- a/docs/docs/install/unraid.md +++ b/docs/docs/install/unraid.md @@ -1,13 +1,19 @@ --- -sidebar_position: 60 +sidebar_position: 70 --- -# Unraid +# Unraid [ Community ] + +:::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/). +::: Immich can easily be installed and updated on Unraid via: -1. [Docker Compose Manager](https://forums.unraid.net/topic/114415-plugin-docker-compose-manager/) plugin from the Unraid Community Apps -2. Community made template on the Unraid Community Apps +1. Community made template on the Unraid Community Apps +2. [Docker Compose Manager](https://forums.unraid.net/topic/114415-plugin-docker-compose-manager/) plugin from the Unraid Community Apps ## Community Applications Template @@ -23,7 +29,7 @@ Once you have Redis and PostgreSQL running, search for Immich on the Unraid CA, For more information about setting up the community image see [here](https://github.com/imagegenius/docker-immich#application-setup) -## Docker-Compose Method (Official) +## Docker-Compose Method :::info From 6b99f02232374530daae373d0e7e306a95a31319 Mon Sep 17 00:00:00 2001 From: Matthew Momjian <50788000+mmomjian@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:37:35 -0400 Subject: [PATCH 15/81] fix(docs): Revise config file instructions and notes (#30418) Revise config file instructions and notes Updated the config file instructions to specify 'immich-config.json' and added notes about interaction with the web UI and microservices. --- docs/docs/install/config-file.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/docs/install/config-file.md b/docs/docs/install/config-file.md index c8ebeffbcd..5c34acdd9d 100644 --- a/docs/docs/install/config-file.md +++ b/docs/docs/install/config-file.md @@ -6,14 +6,18 @@ sidebar_position: 100 A config file can be provided as an alternative to the UI configuration. +:::note Interaction with the web UI +While the config file does not need to include all keys from the below example, specifying `IMMICH_CONFIG_FILE` will disable the ability to edit other properties from the Immich web UI. +::: + ### Step 1 - Create a new config file -In JSON format, create a new config file (e.g. `immich.json`) and put it in a location mounted in the container that can be accessed by Immich. +In JSON format, create a new config file (e.g. `immich-config.json`) and put it in a location mounted in the container that can be accessed by Immich. YAML-formatted config files are also supported. The default configuration looks like this:
-immich.json +immich-config.json ```json { @@ -250,6 +254,10 @@ So you can just grab it from there, paste it into a file and you're pretty much ### Step 2 - Specify the file location +:::note +If you have any `microservices` workers, they will also need to have the config file mounted to their container. +::: + In your `.env` file, set the variable `IMMICH_CONFIG_FILE` to the path of your config. For more information, refer to the [Environment Variables](/install/environment-variables.md) section. @@ -261,7 +269,7 @@ It is recommended to reuse this variable in your `docker-compose.yml`: ```yaml volumes: - - ./configuration.yml:${IMMICH_CONFIG_FILE} + - ./immich-config.json:${IMMICH_CONFIG_FILE} ``` ::: From e7aace436d2d6dcadd356a0f67ae2216d9162b05 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Thu, 30 Jul 2026 12:39:24 -0700 Subject: [PATCH 16/81] chore(mobile): Apply stricter linting rules for correctness (#30372) * chore(mobile): Apply stricter linting rules for correctness * Added discarded_futures rule --- mobile/analysis_options.yaml | 12 +- .../domain/models/user_metadata.model.dart | 8 +- mobile/lib/domain/services/hash.service.dart | 2 +- .../domain/services/local_sync.service.dart | 2 +- .../lib/domain/services/timeline.service.dart | 46 ++-- mobile/lib/domain/utils/event_stream.dart | 4 +- .../entities/asset_edit.entity.dart | 2 +- .../entities/user_metadata.entity.dart | 2 +- .../repositories/sync_stream.repository.dart | 8 +- mobile/lib/main.dart | 26 ++- .../lib/pages/backup/drift_backup.page.dart | 35 +-- .../drift_backup_album_selection.page.dart | 74 +++--- mobile/lib/pages/common/app_log.page.dart | 8 +- .../lib/pages/common/app_log_detail.page.dart | 22 +- mobile/lib/pages/common/download_panel.dart | 4 +- .../lib/pages/common/splash_screen.page.dart | 14 +- mobile/lib/pages/common/tab_shell.page.dart | 4 +- .../lib/pages/library/folder/folder.page.dart | 22 +- .../pages/library/locked/pin_auth.page.dart | 10 +- .../library/shared_link/shared_link.page.dart | 4 +- mobile/lib/pages/login/login.page.dart | 6 +- .../search/map/map_location_picker.page.dart | 3 +- .../pages/share_intent/share_intent.page.dart | 4 +- .../pages/download_info.page.dart | 4 +- .../pages/drift_activities.page.dart | 12 +- .../presentation/pages/drift_album.page.dart | 2 +- .../pages/drift_album_options.page.dart | 26 ++- .../pages/drift_asset_troubleshoot.page.dart | 14 +- .../pages/drift_locked_folder.page.dart | 6 +- .../presentation/pages/drift_map.page.dart | 6 +- .../presentation/pages/drift_memory.page.dart | 22 +- .../pages/drift_people_collection.page.dart | 4 +- .../presentation/pages/drift_person.page.dart | 6 +- .../pages/drift_slideshow.page.dart | 38 +-- .../pages/drift_user_selection.page.dart | 8 +- .../pages/search/drift_search.page.dart | 220 ++++++++++-------- .../search/paginated_search.provider.dart | 2 +- .../add_action_button.widget.dart | 40 ++-- .../cast_action_button.widget.dart | 4 +- ...ownload_status_floating_button.widget.dart | 4 +- ..._profile_picture_action_button.widget.dart | 8 +- .../share_action_button.widget.dart | 48 ++-- .../slideshow_action_button.widget.dart | 8 +- .../widgets/album/album_selector.widget.dart | 10 +- .../album/pending_uploads_banner.widget.dart | 8 +- .../location_details.widget.dart | 4 +- .../asset_details/people_details.widget.dart | 4 +- .../asset_viewer/asset_page.widget.dart | 12 +- .../asset_viewer/asset_viewer.page.dart | 28 +-- .../asset_viewer/ocr_overlay.widget.dart | 2 +- .../asset_viewer/sheet_tile.widget.dart | 4 +- .../asset_viewer/video_viewer.widget.dart | 4 +- .../viewer_top_app_bar.widget.dart | 14 +- .../base_bottom_sheet.widget.dart | 12 +- .../feature_message_dialog.widget.dart | 3 +- .../widgets/images/full_image.widget.dart | 4 +- .../widgets/images/image_provider.dart | 13 +- .../widgets/images/thumbnail.widget.dart | 5 +- .../presentation/widgets/map/map.state.dart | 14 +- .../presentation/widgets/map/map.widget.dart | 2 +- .../widgets/memory/memory_lane.widget.dart | 4 +- .../widgets/timeline/header.widget.dart | 4 +- .../widgets/timeline/scrubber.widget.dart | 12 +- .../widgets/timeline/timeline.widget.dart | 34 +-- .../providers/app_life_cycle.provider.dart | 2 +- .../asset_viewer/asset_viewer.provider.dart | 4 +- .../share_intent_upload.provider.dart | 3 +- .../asset_viewer/video_player_provider.dart | 18 +- .../backup/backup_album.provider.dart | 4 +- mobile/lib/providers/cast.provider.dart | 4 +- mobile/lib/providers/cleanup.provider.dart | 12 +- .../gallery_permission.provider.dart | 3 +- .../providers/haptic_feedback.provider.dart | 12 +- .../readonly_mode.provider.dart | 6 +- mobile/lib/providers/local_auth.provider.dart | 10 +- .../lib/providers/map/map_state.provider.dart | 12 +- mobile/lib/providers/permission.provider.dart | 2 +- .../lib/providers/server_info.provider.dart | 4 +- .../lib/providers/shared_link.provider.dart | 2 +- mobile/lib/providers/user.provider.dart | 2 +- mobile/lib/providers/websocket.provider.dart | 8 +- .../lib/routing/app_navigation_observer.dart | 12 +- .../services/background_upload.service.dart | 6 +- mobile/lib/services/map.service.dart | 4 +- mobile/lib/services/share_intent_service.dart | 4 +- mobile/lib/utils/async_mutex.dart | 10 +- .../utils/hooks/app_settings_update_hook.dart | 4 +- mobile/lib/utils/image_converter.dart | 16 +- .../asset_viewer/animated_play_pause.dart | 5 +- .../backup/drift_album_info_list_tile.dart | 12 +- .../common/app_bar_dialog/app_bar_dialog.dart | 10 +- .../app_bar_dialog/app_bar_server_info.dart | 6 +- .../server_update_notification.dart | 7 +- .../widgets/common/dropdown_search_menu.dart | 4 +- .../common/immich_loading_indicator.dart | 11 +- .../widgets/common/immich_sliver_app_bar.dart | 5 +- .../common/mesmerizing_sliver_app_bar.dart | 20 +- .../widgets/common/person_sliver_app_bar.dart | 20 +- .../common/remote_album_sliver_app_bar.dart | 20 +- .../lib/widgets/forms/login/login_form.dart | 5 +- .../src/controller/photo_view_controller.dart | 2 +- .../photo_view_scalestate_controller.dart | 2 +- .../photo_view/src/core/photo_view_core.dart | 17 +- .../photo_view/src/photo_view_wrappers.dart | 2 - .../widgets/settings/advanced_settings.dart | 12 +- .../asset_list_layout_settings.dart | 4 +- .../asset_list_settings.dart | 4 +- .../image_viewer_quality_setting.dart | 4 +- .../image_viewer_tap_to_navigate_setting.dart | 4 +- .../slideshow_settings.dart | 10 +- .../video_viewer_settings.dart | 8 +- .../drift_backup_settings.dart | 2 +- .../sync_status_and_actions.dart | 6 +- .../settings/free_up_space_settings.dart | 12 +- .../networking_settings/endpoint_input.dart | 4 +- .../external_network_preference.dart | 6 +- .../networking_settings.dart | 6 +- .../settings/notification_setting.dart | 22 +- .../primary_color_setting.dart | 8 +- .../preference_settings/share_setting.dart | 4 +- .../preference_settings/theme_setting.dart | 8 +- mobile/lib/wm_executor.dart | 44 ++-- .../packages/ui/test/formatted_text_test.dart | 2 +- .../widgets/timeline/timeline_args_test.dart | 2 + .../test/services/deep_link_service_test.dart | 6 +- 125 files changed, 859 insertions(+), 632 deletions(-) diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index b9f18d0d81..3f5a33b2b2 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -28,7 +28,6 @@ linter: rules: # Formatting avoid_print: true - unawaited_futures: true require_trailing_commas: true unrelated_type_equality_checks: true prefer_const_constructors: true @@ -50,6 +49,17 @@ linter: avoid_multiple_declarations_per_line: true unnecessary_breaks: true + # Correctness + no_adjacent_strings_in_list: true + cancel_subscriptions: true + close_sinks: true + unawaited_futures: true + discarded_futures: true + no_self_assignments: true + throw_in_finally: true + collection_methods_unrelated_type: true + cast_nullable_to_non_nullable: true + # Known issues avoid_slow_async_io: true avoid_type_to_string: true diff --git a/mobile/lib/domain/models/user_metadata.model.dart b/mobile/lib/domain/models/user_metadata.model.dart index b73f798255..0e702ba868 100644 --- a/mobile/lib/domain/models/user_metadata.model.dart +++ b/mobile/lib/domain/models/user_metadata.model.dart @@ -23,7 +23,7 @@ class Onboarding { } factory Onboarding.fromMap(Map map) { - return Onboarding(isOnboarded: map["isOnboarded"] as bool); + return Onboarding(isOnboarded: map["isOnboarded"]! as bool); } @override @@ -195,9 +195,9 @@ class License { factory License.fromMap(Map map) { return License( - activatedAt: DateTime.parse(map["activatedAt"] as String), - activationKey: map["activationKey"] as String, - licenseKey: map["licenseKey"] as String, + activatedAt: DateTime.parse(map["activatedAt"]! as String), + activationKey: map["activationKey"]! as String, + licenseKey: map["licenseKey"]! as String, ); } diff --git a/mobile/lib/domain/services/hash.service.dart b/mobile/lib/domain/services/hash.service.dart index e4c332b283..b0dbf8fbea 100644 --- a/mobile/lib/domain/services/hash.service.dart +++ b/mobile/lib/domain/services/hash.service.dart @@ -32,7 +32,7 @@ class HashService { }) : _batchSize = batchSize ?? kBatchHashFileLimit { // Stop the in-flight native hash call promptly on cancellation; the loops // below also observe [isCancelled] to bail between batches. - _cancellation?.future.then((_) => _nativeSyncApi.cancelHashing().onError(_log.warning)); + unawaited(_cancellation?.future.then((_) => _nativeSyncApi.cancelHashing().onError(_log.warning))); } bool get isCancelled => _cancellation?.isCompleted ?? false; diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart index feb104f90d..b4ebc66f89 100644 --- a/mobile/lib/domain/services/local_sync.service.dart +++ b/mobile/lib/domain/services/local_sync.service.dart @@ -40,7 +40,7 @@ class LocalSyncService { required this._permissionRepository, this._cancellation, }) { - _cancellation?.future.then((_) => _nativeSyncApi.cancelSync().onError(_log.warning)); + unawaited(_cancellation?.future.then((_) => _nativeSyncApi.cancelSync().onError(_log.warning))); } bool get _isCancelled => _cancellation?.isCompleted ?? false; diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index 9b539ec218..b20ba306ff 100644 --- a/mobile/lib/domain/services/timeline.service.dart +++ b/mobile/lib/domain/services/timeline.service.dart @@ -106,32 +106,34 @@ class TimelineService { TimelineService._({required this._assetSource, required this._bucketSource, required this.origin}) { _bucketSubscription = _bucketSource().listen((buckets) { - _mutex.run(() async { - final totalAssets = buckets.fold(0, (acc, bucket) => acc + bucket.assetCount); + unawaited( + _mutex.run(() async { + final totalAssets = buckets.fold(0, (acc, bucket) => acc + bucket.assetCount); - if (totalAssets == 0) { - _bufferOffset = 0; - _buffer = []; - } else { - final int offset; - final int count; - // When the buffer is empty or the old bufferOffset is greater than the new total assets, - // we need to reset the buffer and load the first batch of assets. - if (_bufferOffset >= totalAssets || _buffer.isEmpty) { - offset = 0; - count = kTimelineAssetLoadBatchSize; + if (totalAssets == 0) { + _bufferOffset = 0; + _buffer = []; } else { - offset = _bufferOffset; - count = math.min(_buffer.length, totalAssets - _bufferOffset); + final int offset; + final int count; + // When the buffer is empty or the old bufferOffset is greater than the new total assets, + // we need to reset the buffer and load the first batch of assets. + if (_bufferOffset >= totalAssets || _buffer.isEmpty) { + offset = 0; + count = kTimelineAssetLoadBatchSize; + } else { + offset = _bufferOffset; + count = math.min(_buffer.length, totalAssets - _bufferOffset); + } + _buffer = await _assetSource(offset, count); + _bufferOffset = offset; } - _buffer = await _assetSource(offset, count); - _bufferOffset = offset; - } - // change the state's total assets count only after the buffer is reloaded - _totalAssets = totalAssets; - EventStream.shared.emit(const TimelineReloadEvent()); - }); + // change the state's total assets count only after the buffer is reloaded + _totalAssets = totalAssets; + EventStream.shared.emit(const TimelineReloadEvent()); + }), + ); }); } diff --git a/mobile/lib/domain/utils/event_stream.dart b/mobile/lib/domain/utils/event_stream.dart index 5967fdca50..0069c75f86 100644 --- a/mobile/lib/domain/utils/event_stream.dart +++ b/mobile/lib/domain/utils/event_stream.dart @@ -32,7 +32,7 @@ class EventStream { } /// Closes the stream controller - void dispose() { - _controller.close(); + Future dispose() { + return _controller.close(); } } diff --git a/mobile/lib/infrastructure/entities/asset_edit.entity.dart b/mobile/lib/infrastructure/entities/asset_edit.entity.dart index 87a05ab8fe..58e1a91d33 100644 --- a/mobile/lib/infrastructure/entities/asset_edit.entity.dart +++ b/mobile/lib/infrastructure/entities/asset_edit.entity.dart @@ -25,7 +25,7 @@ class AssetEditEntity extends Table with DriftDefaultsMixin { } final JsonTypeConverter2, Uint8List, Object?> editParameterConverter = TypeConverter.jsonb( - fromJson: (json) => json as Map, + fromJson: (json) => json! as Map, ); extension AssetEditEntityDataDomainEx on AssetEditEntityData { diff --git a/mobile/lib/infrastructure/entities/user_metadata.entity.dart b/mobile/lib/infrastructure/entities/user_metadata.entity.dart index ede3de3966..da2070fd71 100644 --- a/mobile/lib/infrastructure/entities/user_metadata.entity.dart +++ b/mobile/lib/infrastructure/entities/user_metadata.entity.dart @@ -17,5 +17,5 @@ class UserMetadataEntity extends Table with DriftDefaultsMixin { } final JsonTypeConverter2, Uint8List, Object?> userMetadataConverter = TypeConverter.jsonb( - fromJson: (json) => json as Map, + fromJson: (json) => json! as Map, ); diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index 844226d49f..c43de69c5d 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -358,12 +358,12 @@ class SyncStreamRepository extends DriftDatabaseRepository { final map = metadata.value as Map; final companion = RemoteAssetCloudIdEntityCompanion( cloudId: Value(map['iCloudId']?.toString()), - createdAt: Value(map['createdAt'] != null ? DateTime.parse(map['createdAt'] as String) : null), + createdAt: Value(map['createdAt'] != null ? DateTime.parse(map['createdAt']! as String) : null), adjustmentTime: Value( - map['adjustmentTime'] != null ? DateTime.parse(map['adjustmentTime'] as String) : null, + map['adjustmentTime'] != null ? DateTime.parse(map['adjustmentTime']! as String) : null, ), - latitude: Value(map['latitude'] != null ? (double.tryParse(map['latitude'] as String)) : null), - longitude: Value(map['longitude'] != null ? (double.tryParse(map['longitude'] as String)) : null), + latitude: Value(map['latitude'] != null ? (double.tryParse(map['latitude']! as String)) : null), + longitude: Value(map['longitude'] != null ? (double.tryParse(map['longitude']! as String)) : null), ); batch.insert( _db.remoteAssetCloudIdEntity, diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 09bcdc752f..58e93891a2 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -128,17 +128,17 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve switch (state) { case AppLifecycleState.resumed: dPrint(() => "[APP STATE] resumed"); - ref.read(appStateProvider.notifier).handleAppResume(); + unawaited(ref.read(appStateProvider.notifier).handleAppResume()); unawaited(ref.read(viewIntentHandlerProvider).onAppResumed()); case AppLifecycleState.inactive: dPrint(() => "[APP STATE] inactive"); ref.read(appStateProvider.notifier).handleAppInactivity(); case AppLifecycleState.paused: dPrint(() => "[APP STATE] paused"); - ref.read(appStateProvider.notifier).handleAppPause(); + unawaited(ref.read(appStateProvider.notifier).handleAppPause()); case AppLifecycleState.detached: dPrint(() => "[APP STATE] detached"); - ref.read(appStateProvider.notifier).handleAppDetached(); + unawaited(ref.read(appStateProvider.notifier).handleAppDetached()); case AppLifecycleState.hidden: dPrint(() => "[APP STATE] hidden"); ref.read(appStateProvider.notifier).handleAppHidden(); @@ -216,17 +216,19 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve @override void initState() { super.initState(); - initApp().then((_) => dPrint(() => "App Init Completed")); + unawaited(initApp().then((_) => dPrint(() => "App Init Completed"))); WidgetsBinding.instance.addPostFrameCallback((_) { // needs to be delayed so that EasyLocalization is working - ref.read(backgroundWorkerFgServiceProvider).enable(); + unawaited(ref.read(backgroundWorkerFgServiceProvider).enable()); if (Platform.isAndroid) { - ref - .read(backgroundWorkerFgServiceProvider) - .saveNotificationMessage( - StaticTranslations.instance.uploading_media, - StaticTranslations.instance.backup_background_service_default_notification, - ); + unawaited( + ref + .read(backgroundWorkerFgServiceProvider) + .saveNotificationMessage( + StaticTranslations.instance.uploading_media, + StaticTranslations.instance.backup_background_service_default_notification, + ), + ); } }); @@ -243,7 +245,7 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve @override void reassemble() { if (kDebugMode) { - NetworkRepository.init(); + unawaited(NetworkRepository.init()); } super.reassemble(); } diff --git a/mobile/lib/pages/backup/drift_backup.page.dart b/mobile/lib/pages/backup/drift_backup.page.dart index 793437579a..cc618dbe60 100644 --- a/mobile/lib/pages/backup/drift_backup.page.dart +++ b/mobile/lib/pages/backup/drift_backup.page.dart @@ -43,7 +43,7 @@ class _DriftBackupPageState extends ConsumerState { void initState() { super.initState(); - WakelockPlus.enable(); + unawaited(WakelockPlus.enable()); final currentUser = ref.read(currentUserProvider); if (currentUser == null) { @@ -69,7 +69,7 @@ class _DriftBackupPageState extends ConsumerState { @override void dispose() { super.dispose(); - WakelockPlus.disable(); + unawaited(WakelockPlus.disable()); } @override @@ -111,7 +111,7 @@ class _DriftBackupPageState extends ConsumerState { title: Text("backup_controller_page_backup".t()), leading: IconButton( onPressed: () { - context.maybePop(true); + unawaited(context.maybePop(true)); }, splashRadius: 24, icon: const Icon(Icons.arrow_back_ios_rounded), @@ -119,7 +119,7 @@ class _DriftBackupPageState extends ConsumerState { actions: [ IconButton( onPressed: () { - context.pushRoute(const DriftBackupOptionsRoute()); + unawaited(context.pushRoute(const DriftBackupOptionsRoute())); }, icon: const Icon(Icons.settings_outlined), tooltip: "backup_options".t(context: context), @@ -207,8 +207,8 @@ class _BackupFooterState extends ConsumerState<_BackupFooter> with WidgetsBindin } } - void showPermissionsDialog() { - showDialog( + Future showPermissionsDialog() { + return showDialog( context: context, builder: (ctx) => AlertDialog( content: Text(context.t.notification_permission_dialog_content), @@ -225,7 +225,7 @@ class _BackupFooterState extends ConsumerState<_BackupFooter> with WidgetsBindin expanded: false, onPressed: () { ContextHelper(context).pop(); - openAppSettings(); + unawaited(openAppSettings()); }, ), ], @@ -233,8 +233,8 @@ class _BackupFooterState extends ConsumerState<_BackupFooter> with WidgetsBindin ); } - void showBatteryOptimizationInfo() { - showDialog( + Future showBatteryOptimizationInfo() { + return showDialog( context: context, barrierDismissible: false, builder: (BuildContext ctx) { @@ -246,7 +246,8 @@ class _BackupFooterState extends ConsumerState<_BackupFooter> with WidgetsBindin labelText: context.t.backup_controller_page_background_battery_info_link, variant: .ghost, expanded: false, - onPressed: () => launchUrl(Uri.parse('https://dontkillmyapp.com'), mode: LaunchMode.externalApplication), + onPressed: () => + unawaited(launchUrl(Uri.parse('https://dontkillmyapp.com'), mode: LaunchMode.externalApplication)), ), ImmichTextButton( labelText: context.t.backup_controller_page_background_battery_info_ok, @@ -279,11 +280,13 @@ class _BackupFooterState extends ConsumerState<_BackupFooter> with WidgetsBindin style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), onPressed: () { - ref.read(notificationPermissionProvider.notifier).requestNotificationPermission().then((p) { - if (p == PermissionStatus.permanentlyDenied) { - showPermissionsDialog(); - } - }); + unawaited( + ref.read(notificationPermissionProvider.notifier).requestNotificationPermission().then((p) { + if (p == PermissionStatus.permanentlyDenied) { + unawaited(showPermissionsDialog()); + } + }), + ); }, ), if (notificationStatus != PermissionStatus.granted && batteryOptimizationStatus != PermissionStatus.granted) @@ -297,7 +300,7 @@ class _BackupFooterState extends ConsumerState<_BackupFooter> with WidgetsBindin textAlign: TextAlign.left, style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceSecondary), ), - onPressed: showBatteryOptimizationInfo, + onPressed: () => unawaited(showBatteryOptimizationInfo()), ), ], TextButton.icon( diff --git a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart index 6589741aab..396f4224a7 100644 --- a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart @@ -214,34 +214,36 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState removeSelection() { + return ref.read(backupAlbumProvider.notifier).deselectAlbum(album); } return Padding( padding: const EdgeInsets.only(right: 8.0), child: GestureDetector( - onTap: removeSelection, + onTap: () => unawaited(removeSelection()), child: AnimatedContainer( duration: const Duration(milliseconds: 200), curve: Curves.easeInOut, @@ -387,7 +389,7 @@ class _SelectedAlbumNameChips extends ConsumerWidget { backgroundColor: context.primaryColor, deleteIconColor: context.isDarkTheme ? Colors.black : Colors.white, deleteIcon: const Icon(Icons.cancel_rounded, size: 15), - onDeleted: removeSelection, + onDeleted: () => unawaited(removeSelection()), ), ), ), @@ -408,12 +410,12 @@ class _ExcludedAlbumNameChips extends ConsumerWidget { children: excludedBackupAlbums.asMap().entries.map((entry) { final album = entry.value; - void removeSelection() { - ref.read(backupAlbumProvider.notifier).deselectAlbum(album); + Future removeSelection() { + return ref.read(backupAlbumProvider.notifier).deselectAlbum(album); } return GestureDetector( - onTap: removeSelection, + onTap: () => unawaited(removeSelection()), child: Padding( padding: const EdgeInsets.only(right: 8.0), child: AnimatedContainer( @@ -427,7 +429,7 @@ class _ExcludedAlbumNameChips extends ConsumerWidget { backgroundColor: Colors.red[300], deleteIconColor: context.scaffoldBackgroundColor, deleteIcon: const Icon(Icons.cancel_rounded, size: 15), - onDeleted: removeSelection, + onDeleted: () => unawaited(removeSelection()), ), ), ), @@ -457,7 +459,7 @@ class _SelectAllButton extends ConsumerWidget { ? () { for (final album in filteredAlbums) { if (album.backupSelection != BackupSelection.selected) { - ref.read(backupAlbumProvider.notifier).selectAlbum(album); + unawaited(ref.read(backupAlbumProvider.notifier).selectAlbum(album)); } } } @@ -477,7 +479,7 @@ class _SelectAllButton extends ConsumerWidget { ? () { for (final album in filteredAlbums) { if (album.backupSelection == BackupSelection.selected) { - ref.read(backupAlbumProvider.notifier).deselectAlbum(album); + unawaited(ref.read(backupAlbumProvider.notifier).deselectAlbum(album)); } } } diff --git a/mobile/lib/pages/common/app_log.page.dart b/mobile/lib/pages/common/app_log.page.dart index 5458d90808..b04d8dc926 100644 --- a/mobile/lib/pages/common/app_log.page.dart +++ b/mobile/lib/pages/common/app_log.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -61,7 +63,7 @@ class AppLogPage extends HookConsumerWidget { size: 20.0, ), onPressed: () { - immichLogger.clearLogs(); + unawaited(immichLogger.clearLogs()); shouldReload.value = !shouldReload.value; }, ), @@ -70,7 +72,7 @@ class AppLogPage extends HookConsumerWidget { return IconButton( icon: Icon(Icons.share_rounded, color: context.primaryColor, semanticLabel: "Share logs", size: 20.0), onPressed: () { - ImmichLogger.shareLogs(iconContext); + unawaited(ImmichLogger.shareLogs(iconContext)); }, ); }, @@ -78,7 +80,7 @@ class AppLogPage extends HookConsumerWidget { ], leading: IconButton( onPressed: () { - context.maybePop(); + unawaited(context.maybePop()); }, icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20.0), ), diff --git a/mobile/lib/pages/common/app_log_detail.page.dart b/mobile/lib/pages/common/app_log_detail.page.dart index ab7668f845..330ca51075 100644 --- a/mobile/lib/pages/common/app_log_detail.page.dart +++ b/mobile/lib/pages/common/app_log_detail.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -33,16 +35,18 @@ class AppLogDetailPage extends HookConsumerWidget { ), IconButton( onPressed: () { - Clipboard.setData(ClipboardData(text: text)).then((_) { - context.scaffoldMessenger.showSnackBar( - SnackBar( - content: Text( - "copied_to_clipboard".tr(), - style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), + unawaited( + Clipboard.setData(ClipboardData(text: text)).then((_) { + context.scaffoldMessenger.showSnackBar( + SnackBar( + content: Text( + "copied_to_clipboard".tr(), + style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor), + ), ), - ), - ); - }); + ); + }), + ); }, icon: Icon(Icons.copy, size: 16.0, color: context.primaryColor), ), diff --git a/mobile/lib/pages/common/download_panel.dart b/mobile/lib/pages/common/download_panel.dart index f39aa07166..267d32fc3d 100644 --- a/mobile/lib/pages/common/download_panel.dart +++ b/mobile/lib/pages/common/download_panel.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:background_downloader/background_downloader.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -15,7 +17,7 @@ class DownloadPanel extends ConsumerWidget { final tasks = ref.watch(downloadStateProvider.select((state) => state.taskProgress)).entries.toList(); void onCancelDownload(String id) { - ref.watch(downloadStateProvider.notifier).cancelDownload(id); + unawaited(ref.watch(downloadStateProvider.notifier).cancelDownload(id)); } return Positioned( diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index 711783bc94..4c417cc87c 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -282,11 +282,13 @@ class SplashScreenPageState extends ConsumerState { @override void initState() { super.initState(); - ref - .read(authProvider.notifier) - .setOpenApiServiceEndpoint() - .then(logConnectionInfo) - .whenComplete(() => resumeSession()); + unawaited( + ref + .read(authProvider.notifier) + .setOpenApiServiceEndpoint() + .then(logConnectionInfo) + .whenComplete(() => resumeSession()), + ); } void logConnectionInfo(String? endpoint) { @@ -327,7 +329,7 @@ class SplashScreenPageState extends ConsumerState { if (syncSuccess) { await Future.wait([ backgroundManager.hashAssets().then((_) { - _resumeBackup(backupProvider); + unawaited(_resumeBackup(backupProvider)); }), _resumeBackup(backupProvider), // TODO: Bring back when the soft freeze issue is addressed diff --git a/mobile/lib/pages/common/tab_shell.page.dart b/mobile/lib/pages/common/tab_shell.page.dart index 2fdcec4054..f834e4ac51 100644 --- a/mobile/lib/pages/common/tab_shell.page.dart +++ b/mobile/lib/pages/common/tab_shell.page.dart @@ -126,7 +126,7 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) { // Album page if (index == kAlbumTabIndex) { - ref.read(remoteAlbumProvider.notifier).refresh(); + unawaited(ref.read(remoteAlbumProvider.notifier).refresh()); } // Library page @@ -168,7 +168,7 @@ class _BottomNavigationBarState extends ConsumerState<_BottomNavigationBar> { @override void dispose() { - _eventSubscription?.cancel(); + unawaited(_eventSubscription?.cancel()); super.dispose(); } diff --git a/mobile/lib/pages/library/folder/folder.page.dart b/mobile/lib/pages/library/folder/folder.page.dart index 6934d7b6c5..69631efe4e 100644 --- a/mobile/lib/pages/library/folder/folder.page.dart +++ b/mobile/lib/pages/library/folder/folder.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -53,7 +55,7 @@ class FolderPage extends HookConsumerWidget { useEffect(() { if (folder == null) { - ref.read(folderStructureProvider.notifier).fetchFolders(sortOrder.value); + unawaited(ref.read(folderStructureProvider.notifier).fetchFolders(sortOrder.value)); } return null; }, []); @@ -72,7 +74,7 @@ class FolderPage extends HookConsumerWidget { void onToggleSortOrder() { final newOrder = sortOrder.value == SortOrder.asc ? SortOrder.desc : SortOrder.asc; - ref.read(folderStructureProvider.notifier).fetchFolders(newOrder); + unawaited(ref.read(folderStructureProvider.notifier).fetchFolders(newOrder)); sortOrder.value = newOrder; } @@ -118,7 +120,7 @@ class FolderContent extends HookConsumerWidget { if (folder == null) { return; } - ref.read(folderRenderListProvider(folder!).notifier).fetchAssets(sortOrder); + unawaited(ref.read(folderRenderListProvider(folder!).notifier).fetchAssets(sortOrder)); return null; }, [folder]); @@ -176,12 +178,14 @@ class FolderContent extends HookConsumerWidget { (index, asset) => LargeLeadingTile( onTap: () { AssetViewer.setAsset(ref, asset); - context.pushRoute( - AssetViewerRoute( - initialIndex: index, - timelineService: ref - .read(timelineFactoryProvider) - .fromAssets(folderAssets, TimelineOrigin.folder), + unawaited( + context.pushRoute( + AssetViewerRoute( + initialIndex: index, + timelineService: ref + .read(timelineFactoryProvider) + .fromAssets(folderAssets, TimelineOrigin.folder), + ), ), ); }, diff --git a/mobile/lib/pages/library/locked/pin_auth.page.dart b/mobile/lib/pages/library/locked/pin_auth.page.dart index 7beda1d47b..2da9a8ddab 100644 --- a/mobile/lib/pages/library/locked/pin_auth.page.dart +++ b/mobile/lib/pages/library/locked/pin_auth.page.dart @@ -38,8 +38,8 @@ class PinAuthPage extends HookConsumerWidget { } } - void enableBiometricAuth() { - showDialog( + Future enableBiometricAuth() { + return showDialog( context: context, builder: (buildContext) { return SimpleDialog( @@ -53,7 +53,7 @@ class PinAuthPage extends HookConsumerWidget { description: 'enable_biometric_auth_description'.tr(), onSuccess: (pinCode) { Navigator.pop(buildContext); - registerBiometric(pinCode); + unawaited(registerBiometric(pinCode)); }, autoFocus: true, icon: Icons.fingerprint_rounded, @@ -83,7 +83,7 @@ class PinAuthPage extends HookConsumerWidget { child: PinVerificationForm( autoFocus: true, onSuccess: (_) { - context.replaceRoute(const DriftLockedFolderRoute()); + unawaited(context.replaceRoute(const DriftLockedFolderRoute())); }, ), ), @@ -93,7 +93,7 @@ class PinAuthPage extends HookConsumerWidget { padding: const EdgeInsets.only(right: 16.0), child: TextButton.icon( icon: const Icon(Icons.fingerprint, size: 28), - onPressed: enableBiometricAuth, + onPressed: () => unawaited(enableBiometricAuth()), label: Text( 'use_biometric'.tr(), style: context.textTheme.labelLarge?.copyWith(color: context.primaryColor, fontSize: 18), diff --git a/mobile/lib/pages/library/shared_link/shared_link.page.dart b/mobile/lib/pages/library/shared_link/shared_link.page.dart index a4f52ebd4c..18fc235dd8 100644 --- a/mobile/lib/pages/library/shared_link/shared_link.page.dart +++ b/mobile/lib/pages/library/shared_link/shared_link.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -17,7 +19,7 @@ class SharedLinkPage extends HookConsumerWidget { final sharedLinks = ref.watch(sharedLinksStateProvider); useEffect(() { - ref.read(sharedLinksStateProvider.notifier).fetchLinks(); + unawaited(ref.read(sharedLinksStateProvider.notifier).fetchLinks()); return () { if (!context.mounted) { return; diff --git a/mobile/lib/pages/login/login.page.dart b/mobile/lib/pages/login/login.page.dart index 79091d2679..e225c4c066 100644 --- a/mobile/lib/pages/login/login.page.dart +++ b/mobile/lib/pages/login/login.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -22,7 +24,7 @@ class LoginPage extends HookConsumerWidget { } useEffect(() { - getAppInfo(); + unawaited(getAppInfo()); return null; }); @@ -55,7 +57,7 @@ class LoginPage extends HookConsumerWidget { ), ), onTap: () { - context.pushRoute(const AppLogRoute()); + unawaited(context.pushRoute(const AppLogRoute())); }, ), ], diff --git a/mobile/lib/pages/search/map/map_location_picker.page.dart b/mobile/lib/pages/search/map/map_location_picker.page.dart index 96f41a4d38..bb848b24bc 100644 --- a/mobile/lib/pages/search/map/map_location_picker.page.dart +++ b/mobile/lib/pages/search/map/map_location_picker.page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:math'; import 'package:auto_route/auto_route.dart'; @@ -37,7 +38,7 @@ class MapLocationPickerPage extends HookConsumerWidget { } void onClose([LatLng? selected]) { - context.maybePop(selected); + unawaited(context.maybePop(selected)); } Future getCurrentLocation() async { diff --git a/mobile/lib/pages/share_intent/share_intent.page.dart b/mobile/lib/pages/share_intent/share_intent.page.dart index ec88c4a9e4..db5e583d7a 100644 --- a/mobile/lib/pages/share_intent/share_intent.page.dart +++ b/mobile/lib/pages/share_intent/share_intent.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -65,7 +67,7 @@ class ShareIntentPage extends ConsumerWidget { ), leading: IconButton( onPressed: () { - context.navigateTo(const TabShellRoute()); + unawaited(context.navigateTo(const TabShellRoute())); }, icon: const Icon(Icons.arrow_back), ), diff --git a/mobile/lib/presentation/pages/download_info.page.dart b/mobile/lib/presentation/pages/download_info.page.dart index af44714b83..c2c63c7860 100644 --- a/mobile/lib/presentation/pages/download_info.page.dart +++ b/mobile/lib/presentation/pages/download_info.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -15,7 +17,7 @@ class DownloadInfoPage extends ConsumerWidget { final tasks = ref.watch(downloadStateProvider.select((state) => state.taskProgress)).entries.toList(); void onCancelDownload(String id) { - ref.watch(downloadStateProvider.notifier).cancelDownload(id); + unawaited(ref.watch(downloadStateProvider.notifier).cancelDownload(id)); } return Scaffold( diff --git a/mobile/lib/presentation/pages/drift_activities.page.dart b/mobile/lib/presentation/pages/drift_activities.page.dart index 59c9a8a1e1..ebf7c2efa7 100644 --- a/mobile/lib/presentation/pages/drift_activities.page.dart +++ b/mobile/lib/presentation/pages/drift_activities.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; @@ -25,13 +27,17 @@ class DriftActivitiesPage extends HookConsumerWidget { final activities = ref.watch(albumActivityProvider((album.id, assetId))); final listViewScrollController = useScrollController(); - void scrollToBottom() { - listViewScrollController.animateTo(0, duration: const Duration(milliseconds: 300), curve: Curves.fastOutSlowIn); + Future scrollToBottom() { + return listViewScrollController.animateTo( + 0, + duration: const Duration(milliseconds: 300), + curve: Curves.fastOutSlowIn, + ); } Future onAddComment(String comment) async { await activityNotifier.addComment(comment); - scrollToBottom(); + unawaited(scrollToBottom()); } return ProviderScope( diff --git a/mobile/lib/presentation/pages/drift_album.page.dart b/mobile/lib/presentation/pages/drift_album.page.dart index 47a4625f87..ab91b6f3c8 100644 --- a/mobile/lib/presentation/pages/drift_album.page.dart +++ b/mobile/lib/presentation/pages/drift_album.page.dart @@ -53,7 +53,7 @@ class _DriftAlbumsPageState extends ConsumerState { ), AlbumSelector( onAlbumSelected: (album) { - context.router.push(RemoteAlbumRoute(album: album)); + unawaited(context.router.push(RemoteAlbumRoute(album: album))); }, ), ], diff --git a/mobile/lib/presentation/pages/drift_album_options.page.dart b/mobile/lib/presentation/pages/drift_album_options.page.dart index 84060aa38c..37c0273fae 100644 --- a/mobile/lib/presentation/pages/drift_album_options.page.dart +++ b/mobile/lib/presentation/pages/drift_album_options.page.dart @@ -110,18 +110,20 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { ]; } - showModalBottomSheet( - backgroundColor: context.colorScheme.surfaceContainer, - isScrollControlled: false, - context: context, - builder: (context) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.only(top: 24.0), - child: Column(mainAxisSize: MainAxisSize.min, children: [...actions]), - ), - ); - }, + unawaited( + showModalBottomSheet( + backgroundColor: context.colorScheme.surfaceContainer, + isScrollControlled: false, + context: context, + builder: (context) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.only(top: 24.0), + child: Column(mainAxisSize: MainAxisSize.min, children: [...actions]), + ), + ); + }, + ), ); } diff --git a/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart b/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart index 3e5b603451..cee0cdc334 100644 --- a/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart +++ b/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -78,11 +80,13 @@ class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection @override void initState() { super.initState(); - _buildAssetProperties(widget.asset).whenComplete(() { - if (mounted) { - setState(() {}); - } - }); + unawaited( + _buildAssetProperties(widget.asset).whenComplete(() { + if (mounted) { + setState(() {}); + } + }), + ); } @override diff --git a/mobile/lib/presentation/pages/drift_locked_folder.page.dart b/mobile/lib/presentation/pages/drift_locked_folder.page.dart index 9849558c94..e8130f7059 100644 --- a/mobile/lib/presentation/pages/drift_locked_folder.page.dart +++ b/mobile/lib/presentation/pages/drift_locked_folder.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -39,8 +41,8 @@ class _DriftLockedFolderPageState extends ConsumerState w return; } if (state == AppLifecycleState.paused) { - ref.read(authProvider.notifier).lockPinCode(); - context.navigateTo(const TabShellRoute()); + unawaited(ref.read(authProvider.notifier).lockPinCode()); + unawaited(context.navigateTo(const TabShellRoute())); return; } setState(() { diff --git a/mobile/lib/presentation/pages/drift_map.page.dart b/mobile/lib/presentation/pages/drift_map.page.dart index 97062b88ab..d36ccc350c 100644 --- a/mobile/lib/presentation/pages/drift_map.page.dart +++ b/mobile/lib/presentation/pages/drift_map.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -11,8 +13,8 @@ class DriftMapPage extends StatelessWidget { const DriftMapPage({super.key, this.initialLocation}); - void onSettingsPressed(BuildContext context) { - showModalBottomSheet( + Future onSettingsPressed(BuildContext context) { + return showModalBottomSheet( elevation: 0.0, showDragHandle: true, isScrollControlled: true, diff --git a/mobile/lib/presentation/pages/drift_memory.page.dart b/mobile/lib/presentation/pages/drift_memory.page.dart index b8f3c94a00..31371fe581 100644 --- a/mobile/lib/presentation/pages/drift_memory.page.dart +++ b/mobile/lib/presentation/pages/drift_memory.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -47,21 +49,21 @@ class DriftMemoryPage extends HookConsumerWidget { useEffect(() { // Memories is an immersive activity - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); + unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive)); return () { // Clean up to normal edge to edge when we are done - restoreEdgeToEdge(); + unawaited(restoreEdgeToEdge()); }; }); void toNextMemory() { - memoryPageController.nextPage(duration: const Duration(milliseconds: 500), curve: Curves.easeIn); + unawaited(memoryPageController.nextPage(duration: const Duration(milliseconds: 500), curve: Curves.easeIn)); } void toPreviousMemory() { if (currentMemoryIndex.value > 0) { // Move to the previous memory page - memoryPageController.previousPage(duration: const Duration(milliseconds: 500), curve: Curves.easeIn); + unawaited(memoryPageController.previousPage(duration: const Duration(milliseconds: 500), curve: Curves.easeIn)); // Wait for the next frame to ensure the page is built SchedulerBinding.instance.addPostFrameCallback((_) { @@ -88,7 +90,7 @@ class DriftMemoryPage extends HookConsumerWidget { // Go to the next asset final PageController controller = memoryAssetPageControllers[currentMemoryIndex.value]; - controller.nextPage(curve: Curves.easeInOut, duration: const Duration(milliseconds: 500)); + unawaited(controller.nextPage(curve: Curves.easeInOut, duration: const Duration(milliseconds: 500))); } else { // Go to the next memory since we are at the end of our assets toNextMemory(); @@ -100,7 +102,7 @@ class DriftMemoryPage extends HookConsumerWidget { // Go to the previous asset final PageController controller = memoryAssetPageControllers[currentMemoryIndex.value]; - controller.previousPage(curve: Curves.easeInOut, duration: const Duration(milliseconds: 500)); + unawaited(controller.previousPage(curve: Curves.easeInOut, duration: const Duration(milliseconds: 500))); } else { // Go to the previous memory since we are at the end of our assets toPreviousMemory(); @@ -153,7 +155,7 @@ class DriftMemoryPage extends HookConsumerWidget { // Precache the next page right away if we are on the first page if (currentAssetPage.value == 0) { - Future.delayed(const Duration(milliseconds: 200)).then((_) => precacheAsset(1)); + unawaited(Future.delayed(const Duration(milliseconds: 200)).then((_) => precacheAsset(1))); } Future onAssetChanged(int otherIndex) async { @@ -198,7 +200,7 @@ class DriftMemoryPage extends HookConsumerWidget { final offset = notification.metrics.pixels; if (isEpiloguePage && (offset > notification.metrics.maxScrollExtent + 150)) { - context.maybePop(); + unawaited(context.maybePop()); return true; } } @@ -328,8 +330,8 @@ class DriftMemoryPage extends HookConsumerWidget { // auto_route doesn't invoke pop scope, so // turn off full screen mode here // https://github.com/Milad-Akarie/auto_route_library/issues/1799 - context.maybePop(); - restoreEdgeToEdge(); + unawaited(context.maybePop()); + unawaited(restoreEdgeToEdge()); }, shape: const CircleBorder(), color: Colors.white.withValues(alpha: 0.2), diff --git a/mobile/lib/presentation/pages/drift_people_collection.page.dart b/mobile/lib/presentation/pages/drift_people_collection.page.dart index f39b5e15c7..416b7d587a 100644 --- a/mobile/lib/presentation/pages/drift_people_collection.page.dart +++ b/mobile/lib/presentation/pages/drift_people_collection.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -86,7 +88,7 @@ class _DriftPeopleCollectionPageState extends ConsumerState { } } - void showOptionSheet(BuildContext context) { - showModalBottomSheet( + Future showOptionSheet(BuildContext context) { + return showModalBottomSheet( context: context, backgroundColor: context.colorScheme.surface, isScrollControlled: false, diff --git a/mobile/lib/presentation/pages/drift_slideshow.page.dart b/mobile/lib/presentation/pages/drift_slideshow.page.dart index 9b7c10c891..81c09bea67 100644 --- a/mobile/lib/presentation/pages/drift_slideshow.page.dart +++ b/mobile/lib/presentation/pages/drift_slideshow.page.dart @@ -67,7 +67,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si _updateNextIndex(); ref.listenManual(appConfigProvider.select((s) => s.slideshow), _onConfigChanged); - SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); + unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive)); unawaited(WakelockPlus.enable()); } @@ -94,9 +94,9 @@ class _DriftSlideshowPageState extends ConsumerState with Si if (asset.isImage) { _createTimer(); } else if (ref.read(videoPlayerProvider(asset.heroTag)).status == VideoPlaybackStatus.paused) { - ref.read(videoPlayerProvider(asset.heroTag).notifier).play(); + unawaited(ref.read(videoPlayerProvider(asset.heroTag).notifier).play()); } else { - _nextPage(); + unawaited(_nextPage()); } _updateNextIndex(); @@ -113,7 +113,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si final asset = widget.timeline.getAssetSafe(_index)!; if (!asset.isImage) { - ref.read(videoPlayerProvider(asset.heroTag).notifier).pause(); + unawaited(ref.read(videoPlayerProvider(asset.heroTag).notifier).pause()); } setState(() { @@ -147,7 +147,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si }; if (!widget.timeline.hasRange(_nextIndex, 1)) { - widget.timeline.preloadAssets(_nextIndex); + unawaited(widget.timeline.preloadAssets(_nextIndex)); } } @@ -184,14 +184,16 @@ class _DriftSlideshowPageState extends ConsumerState with Si _crossfadeFromIndex = previousIndex; _crossfadeToIndex = page; }); - _crossfadeController.forward(from: 0.0).whenComplete(() { - if (mounted) { - setState(() { - _crossfadeFromIndex = null; - _crossfadeToIndex = null; - }); - } - }); + unawaited( + _crossfadeController.forward(from: 0.0).whenComplete(() { + if (mounted) { + setState(() { + _crossfadeFromIndex = null; + _crossfadeToIndex = null; + }); + } + }), + ); } Widget _getCrossfadeLayer(BuildContext context, int index, {required bool isIncoming}) { @@ -238,7 +240,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si _timer = Timer(Duration(milliseconds: _config.duration * 1000 - _stopwatch.elapsedMilliseconds), () { _stopwatch.stop(); _stopwatch.reset(); - _nextPage(); + unawaited(_nextPage()); }); _stopwatch.start(); @@ -376,9 +378,9 @@ class _DriftSlideshowPageState extends ConsumerState with Si final position = ref.read(videoPlayerProvider(asset.heroTag)).position; if (status == VideoPlaybackStatus.completed && isCurrent && position.inMicroseconds > 0) { - _nextPage(); + unawaited(_nextPage()); } else if (status == VideoPlaybackStatus.playing) { - ref.read(videoPlayerProvider(asset.heroTag).notifier).setLoop(false); + unawaited(ref.read(videoPlayerProvider(asset.heroTag).notifier).setLoop(false)); } return PhotoView.customChild( @@ -418,7 +420,7 @@ class _DriftSlideshowPageState extends ConsumerState with Si IconButton( onPressed: () { _pause(); - context.pushRoute(SettingsSubRoute(section: SettingSection.assetViewer)); + unawaited(context.pushRoute(SettingsSubRoute(section: SettingSection.assetViewer))); }, icon: const Icon(Icons.settings), ), @@ -512,7 +514,7 @@ class _SlideshowProgressBarState extends State<_SlideshowProgressBar> with Singl animationBehavior: AnimationBehavior.preserve, )..value = (widget.elapsedMs / widget.durationMs).clamp(0.0, 1.0); if (!widget.paused) { - _controller.forward(); + unawaited(_controller.forward()); } } diff --git a/mobile/lib/presentation/pages/drift_user_selection.page.dart b/mobile/lib/presentation/pages/drift_user_selection.page.dart index 41394014a0..19a450b435 100644 --- a/mobile/lib/presentation/pages/drift_user_selection.page.dart +++ b/mobile/lib/presentation/pages/drift_user_selection.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -53,7 +55,7 @@ class DriftUserSelectionPage extends HookConsumerWidget { final sharedUsersList = useState>({}); void addNewUsersHandler() { - context.maybePop(sharedUsersList.value.map((e) => e.id).toList()); + unawaited(context.maybePop(sharedUsersList.value.map((e) => e.id).toList())); } Widget buildTileIcon(UserDto user) { @@ -122,12 +124,12 @@ class DriftUserSelectionPage extends HookConsumerWidget { leading: IconButton( icon: const Icon(Icons.close_rounded), onPressed: () { - context.maybePop(null); + unawaited(context.maybePop(null)); }, ), actions: [ TextButton( - onPressed: sharedUsersList.value.isEmpty ? null : addNewUsersHandler, + onPressed: sharedUsersList.value.isEmpty ? null : () => addNewUsersHandler(), child: const Text("add", style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)).tr(), ), ], diff --git a/mobile/lib/presentation/pages/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/drift_search.page.dart index 4d04967b28..8d6122804a 100644 --- a/mobile/lib/presentation/pages/search/drift_search.page.dart +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -105,20 +105,22 @@ class DriftSearchPage extends HookConsumerWidget { return null; } - Future.microtask(() { - textSearchController.clear(); - peopleCurrentFilterWidget.value = null; - dateRangeCurrentFilterWidget.value = null; - cameraCurrentFilterWidget.value = null; - tagCurrentFilterWidget.value = null; - mediaTypeCurrentFilterWidget.value = null; - ratingCurrentFilterWidget.value = null; - displayOptionCurrentFilterWidget.value = null; - locationCurrentFilterWidget.value = preFilter.location.city != null - ? Text(preFilter.location.city!, style: context.textTheme.labelLarge) - : null; - search(preFilter); - }); + unawaited( + Future.microtask(() { + textSearchController.clear(); + peopleCurrentFilterWidget.value = null; + dateRangeCurrentFilterWidget.value = null; + cameraCurrentFilterWidget.value = null; + tagCurrentFilterWidget.value = null; + mediaTypeCurrentFilterWidget.value = null; + ratingCurrentFilterWidget.value = null; + displayOptionCurrentFilterWidget.value = null; + locationCurrentFilterWidget.value = preFilter.location.city != null + ? Text(preFilter.location.city!, style: context.textTheme.labelLarge) + : null; + search(preFilter); + }), + ); return null; }, [preFilter]); @@ -141,17 +143,19 @@ class DriftSearchPage extends HookConsumerWidget { search(filter.value.copyWith(people: people)); } - showFilterBottomSheet( - context: context, - isScrollControlled: true, - child: FractionallySizedBox( - heightFactor: 0.8, - child: FilterBottomSheetScaffold( - title: 'search_filter_people_title'.t(context: context), - expanded: true, - onSearch: handleApply, - onClear: handleClear, - child: PeoplePicker(onSelect: handleOnSelect, filter: filter.value.people), + unawaited( + showFilterBottomSheet( + context: context, + isScrollControlled: true, + child: FractionallySizedBox( + heightFactor: 0.8, + child: FilterBottomSheetScaffold( + title: 'search_filter_people_title'.t(context: context), + expanded: true, + onSearch: handleApply, + onClear: handleClear, + child: PeoplePicker(onSelect: handleOnSelect, filter: filter.value.people), + ), ), ), ); @@ -176,17 +180,19 @@ class DriftSearchPage extends HookConsumerWidget { search(filter.value.copyWith(tagIds: tagIds)); } - showFilterBottomSheet( - context: context, - isScrollControlled: true, - child: FractionallySizedBox( - heightFactor: 0.8, - child: FilterBottomSheetScaffold( - title: 'search_filter_tags_title'.t(context: context), - expanded: true, - onSearch: handleApply, - onClear: handleClear, - child: TagPicker(onSelectExistingTag: handleOnSelect, filter: (filter.value.tagIds ?? []).toSet()), + unawaited( + showFilterBottomSheet( + context: context, + isScrollControlled: true, + child: FractionallySizedBox( + heightFactor: 0.8, + child: FilterBottomSheetScaffold( + title: 'search_filter_tags_title'.t(context: context), + expanded: true, + onSearch: handleApply, + onClear: handleClear, + child: TagPicker(onSelectExistingTag: handleOnSelect, filter: (filter.value.tagIds ?? []).toSet()), + ), ), ), ); @@ -216,21 +222,23 @@ class DriftSearchPage extends HookConsumerWidget { search(filter.value.copyWith(location: location)); } - showFilterBottomSheet( - context: context, - isScrollControlled: true, - isDismissible: true, - child: FilterBottomSheetScaffold( - title: 'search_filter_location_title'.t(context: context), - onSearch: handleApply, - onClear: handleClear, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: Container( - padding: EdgeInsets.only(bottom: context.viewInsets.bottom), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: LocationPicker(onSelected: handleOnSelect, filter: filter.value.location), + unawaited( + showFilterBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: true, + child: FilterBottomSheetScaffold( + title: 'search_filter_location_title'.t(context: context), + onSearch: handleApply, + onClear: handleClear, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Container( + padding: EdgeInsets.only(bottom: context.viewInsets.bottom), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: LocationPicker(onSelected: handleOnSelect, filter: filter.value.location), + ), ), ), ), @@ -259,17 +267,19 @@ class DriftSearchPage extends HookConsumerWidget { search(filter.value.copyWith(camera: camera)); } - showFilterBottomSheet( - context: context, - isScrollControlled: true, - isDismissible: true, - child: FilterBottomSheetScaffold( - title: 'search_filter_camera_title'.t(context: context), - onSearch: handleApply, - onClear: handleClear, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: CameraPicker(onSelect: handleOnSelect, filter: filter.value.camera), + unawaited( + showFilterBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: true, + child: FilterBottomSheetScaffold( + title: 'search_filter_camera_title'.t(context: context), + onSearch: handleApply, + onClear: handleClear, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: CameraPicker(onSelect: handleOnSelect, filter: filter.value.camera), + ), ), ), ); @@ -339,22 +349,24 @@ class DriftSearchPage extends HookConsumerWidget { } void showQuickDatePicker() { - showFilterBottomSheet( - context: context, - child: FilterBottomSheetScaffold( - title: "pick_date_range".tr(), - expanded: true, - onClear: () => datePicked(null), - child: QuickDatePicker( - currentInput: dateInputFilter.value, - onRequestPicker: () { - ContextHelper(context).pop(); - showDatePicker(); - }, - onSelect: (date) { - ContextHelper(context).pop(); - datePicked(date); - }, + unawaited( + showFilterBottomSheet( + context: context, + child: FilterBottomSheetScaffold( + title: "pick_date_range".tr(), + expanded: true, + onClear: () => datePicked(null), + child: QuickDatePicker( + currentInput: dateInputFilter.value, + onRequestPicker: () { + ContextHelper(context).pop(); + unawaited(showDatePicker()); + }, + onSelect: (date) { + ContextHelper(context).pop(); + datePicked(date); + }, + ), ), ), ); @@ -383,13 +395,15 @@ class DriftSearchPage extends HookConsumerWidget { search(filter.value.copyWith(mediaType: mediaType)); } - showFilterBottomSheet( - context: context, - child: FilterBottomSheetScaffold( - title: 'search_filter_media_type_title'.t(context: context), - onSearch: handleApply, - onClear: handleClear, - child: MediaTypePicker(onSelect: handleOnSelected, filter: filter.value.mediaType), + unawaited( + showFilterBottomSheet( + context: context, + child: FilterBottomSheetScaffold( + title: 'search_filter_media_type_title'.t(context: context), + onSearch: handleApply, + onClear: handleClear, + child: MediaTypePicker(onSelect: handleOnSelected, filter: filter.value.mediaType), + ), ), ); } @@ -417,14 +431,16 @@ class DriftSearchPage extends HookConsumerWidget { search(filter.value.copyWith(rating: rating)); } - showFilterBottomSheet( - context: context, - isScrollControlled: true, - child: FilterBottomSheetScaffold( - title: 'rating'.t(context: context), - onSearch: handleApply, - onClear: handleClear, - child: StarRatingPicker(onSelect: handleOnSelected, filter: filter.value.rating), + unawaited( + showFilterBottomSheet( + context: context, + isScrollControlled: true, + child: FilterBottomSheetScaffold( + title: 'rating'.t(context: context), + onSearch: handleApply, + onClear: handleClear, + child: StarRatingPicker(onSelect: handleOnSelected, filter: filter.value.rating), + ), ), ); } @@ -462,13 +478,15 @@ class DriftSearchPage extends HookConsumerWidget { search(filter.value.copyWith(display: display)); } - showFilterBottomSheet( - context: context, - child: FilterBottomSheetScaffold( - title: 'display_options'.t(context: context), - onSearch: handleApply, - onClear: handleClear, - child: DisplayOptionPicker(onSelect: handleOnSelect, filter: filter.value.display), + unawaited( + showFilterBottomSheet( + context: context, + child: FilterBottomSheetScaffold( + title: 'display_options'.t(context: context), + onSearch: handleApply, + onClear: handleClear, + child: DisplayOptionPicker(onSelect: handleOnSelect, filter: filter.value.display), + ), ), ); } @@ -697,7 +715,7 @@ class DriftSearchPage extends HookConsumerWidget { if (filter.value.isEmpty) const _SearchSuggestions() else - _SearchResultGrid(onScrollEnd: loadMoreSearchResults), + _SearchResultGrid(onScrollEnd: () => loadMoreSearchResults()), ], ), ); diff --git a/mobile/lib/presentation/pages/search/paginated_search.provider.dart b/mobile/lib/presentation/pages/search/paginated_search.provider.dart index fa2d5a06cf..e9839f31ea 100644 --- a/mobile/lib/presentation/pages/search/paginated_search.provider.dart +++ b/mobile/lib/presentation/pages/search/paginated_search.provider.dart @@ -70,7 +70,7 @@ class PaginatedSearchNotifier extends StateNotifier { @override void dispose() { - _assetCountController.close(); + unawaited(_assetCountController.close()); super.dispose(); } } diff --git a/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart index 86d3fa0749..bcd3b20df6 100644 --- a/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -36,11 +38,11 @@ class _AddActionButtonState extends ConsumerState { case AddToMenuItem.album: _openAlbumSelector(); case AddToMenuItem.archive: - performArchiveAction(context, ref, source: ActionSource.viewer); + unawaited(performArchiveAction(context, ref, source: ActionSource.viewer)); case AddToMenuItem.unarchive: - performUnArchiveAction(context, ref, source: ActionSource.viewer); + unawaited(performUnArchiveAction(context, ref, source: ActionSource.viewer)); case AddToMenuItem.lockedFolder: - performMoveToLockFolderAction(context, ref, source: ActionSource.viewer); + unawaited(performMoveToLockFolderAction(context, ref, source: ActionSource.viewer)); } } @@ -112,21 +114,23 @@ class _AddActionButtonState extends ConsumerState { AlbumSelector(onAlbumSelected: (album) => _addCurrentAssetToAlbum(album)), ]; - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (_) { - return BaseBottomSheet( - actions: const [], - slivers: slivers, - initialChildSize: 0.6, - minChildSize: 0.3, - maxChildSize: 0.95, - expand: false, - backgroundColor: context.isDarkTheme ? Colors.black : Colors.white, - ); - }, + unawaited( + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) { + return BaseBottomSheet( + actions: const [], + slivers: slivers, + initialChildSize: 0.6, + minChildSize: 0.3, + maxChildSize: 0.95, + expand: false, + backgroundColor: context.isDarkTheme ? Colors.black : Colors.white, + ); + }, + ), ); } diff --git a/mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart index 7a4f84fb4f..9465f50500 100644 --- a/mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -21,7 +23,7 @@ class CastActionButton extends ConsumerWidget { iconColor: isCasting ? context.primaryColor : null, // null = default color label: "cast".t(context: context), onPressed: () { - showDialog(context: context, builder: (context) => const CastDialog()); + unawaited(showDialog(context: context, builder: (context) => const CastDialog())); }, iconOnly: iconOnly, menuItem: menuItem, diff --git a/mobile/lib/presentation/widgets/action_buttons/download_status_floating_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/download_status_floating_button.widget.dart index efa7f5c6d0..264e489db3 100644 --- a/mobile/lib/presentation/widgets/action_buttons/download_status_floating_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/download_status_floating_button.widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -33,7 +35,7 @@ class DownloadStatusFloatingButton extends ConsumerWidget { : context.colorScheme.surfaceBright, elevation: 2, onPressed: () { - context.pushRoute(const DownloadInfoRoute()); + unawaited(context.pushRoute(const DownloadInfoRoute())); }, child: Stack( alignment: AlignmentDirectional.center, diff --git a/mobile/lib/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart index c8dbb7cb1f..5b41715022 100644 --- a/mobile/lib/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -13,12 +15,12 @@ class SetProfilePictureActionButton extends ConsumerWidget { const SetProfilePictureActionButton({super.key, required this.asset, this.iconOnly = false, this.menuItem = false}); - void _onTap(BuildContext context) { + Future _onTap(BuildContext context) async { if (!context.mounted) { return; } - context.pushRoute(ProfilePictureCropRoute(asset: asset)); + await context.pushRoute(ProfilePictureCropRoute(asset: asset)); } @override @@ -28,7 +30,7 @@ class SetProfilePictureActionButton extends ConsumerWidget { label: "set_as_profile_picture".t(context: context), iconOnly: iconOnly, menuItem: menuItem, - onPressed: () => _onTap(context), + onPressed: () => unawaited(_onTap(context)), maxWidth: 100, ); } 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 ef520ea941..eadcf0a81e 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 @@ -138,31 +138,33 @@ class ShareActionButton extends ConsumerWidget { await showDialog( context: context, builder: (BuildContext buildContext) { - ref - .read(actionProvider.notifier) - .shareAssets( - source, - context, - fileType: fileType, - cancelCompleter: cancelCompleter, - onAssetDownloadProgress: (value) => progress.value = value, - ) - .then((ActionResult result) { - if (cancelCompleter.isCompleted || !context.mounted) { - return; - } + unawaited( + ref + .read(actionProvider.notifier) + .shareAssets( + source, + context, + fileType: fileType, + cancelCompleter: cancelCompleter, + onAssetDownloadProgress: (value) => progress.value = value, + ) + .then((ActionResult result) { + if (cancelCompleter.isCompleted || !context.mounted) { + return; + } - if (!result.success) { - ImmichToast.show( - context: context, - msg: context.t.scaffold_body_error_occurred, - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - } + if (!result.success) { + ImmichToast.show( + context: context, + msg: context.t.scaffold_body_error_occurred, + gravity: ToastGravity.BOTTOM, + toastType: ToastType.error, + ); + } - buildContext.pop(); - }); + buildContext.pop(); + }), + ); return preparingDialog; }, diff --git a/mobile/lib/presentation/widgets/action_buttons/slideshow_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/slideshow_action_button.widget.dart index 479cf2dfe9..fdbc7a8cda 100644 --- a/mobile/lib/presentation/widgets/action_buttons/slideshow_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/slideshow_action_button.widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -12,12 +14,12 @@ class SlideshowActionButton extends ConsumerWidget { const SlideshowActionButton({super.key, this.iconOnly = false, this.menuItem = false}); - void _onTap(BuildContext context, WidgetRef ref) { + Future _onTap(BuildContext context, WidgetRef ref) async { if (!context.mounted) { return; } - context.pushRoute(DriftSlideshowRoute(timeline: ref.read(timelineServiceProvider))); + await context.pushRoute(DriftSlideshowRoute(timeline: ref.read(timelineServiceProvider))); } @override @@ -27,7 +29,7 @@ class SlideshowActionButton extends ConsumerWidget { label: "slideshow".t(context: context), iconOnly: iconOnly, menuItem: menuItem, - onPressed: () => _onTap(context, ref), + onPressed: () => unawaited(_onTap(context, ref)), maxWidth: 100, ); } diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index ba6c6da560..bf5de5611d 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -63,7 +63,7 @@ class _AlbumSelectorState extends ConsumerState { isGrid = albumConfig.isGrid; }); - ref.read(remoteAlbumProvider.notifier).refresh(); + unawaited(ref.read(remoteAlbumProvider.notifier).refresh()); }); searchController.addListener(() { @@ -81,7 +81,7 @@ class _AlbumSelectorState extends ConsumerState { final userId = ref.read(currentUserProvider)?.id; filter = filter.copyWith(query: searchTerm, userId: userId, mode: filterMode); - filterAlbums(); + unawaited(filterAlbums()); } Future onRefresh() async { @@ -92,7 +92,7 @@ class _AlbumSelectorState extends ConsumerState { setState(() { isGrid = !isGrid; }); - ref.read(settingsProvider).write(.albumIsGrid, isGrid); + unawaited(ref.read(settingsProvider).write(.albumIsGrid, isGrid)); } void changeFilter(QuickFilterMode mode) { @@ -100,7 +100,7 @@ class _AlbumSelectorState extends ConsumerState { filter = filter.copyWith(mode: mode); }); - filterAlbums(); + unawaited(filterAlbums()); } Future changeSort(AlbumSort sort) async { @@ -121,7 +121,7 @@ class _AlbumSelectorState extends ConsumerState { searchController.clear(); }); - filterAlbums(); + unawaited(filterAlbums()); } Future sortAlbums() async { diff --git a/mobile/lib/presentation/widgets/album/pending_uploads_banner.widget.dart b/mobile/lib/presentation/widgets/album/pending_uploads_banner.widget.dart index 2701316e75..5622ed6b1b 100644 --- a/mobile/lib/presentation/widgets/album/pending_uploads_banner.widget.dart +++ b/mobile/lib/presentation/widgets/album/pending_uploads_banner.widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; @@ -45,8 +47,8 @@ class PendingUploadsBanner extends ConsumerWidget { ); } - static void _openSheet(BuildContext context, String albumId) { - showModalBottomSheet( + static Future _openSheet(BuildContext context, String albumId) { + return showModalBottomSheet( context: context, showDragHandle: true, builder: (_) => _PendingUploadsSheet(albumId: albumId), @@ -97,7 +99,7 @@ class _PendingUploadsBannerContent extends StatelessWidget { return Material( color: hasFailures ? context.colorScheme.errorContainer : context.colorScheme.surfaceContainerHigh, child: InkWell( - onTap: () => PendingUploadsBanner._openSheet(context, albumId), + onTap: () => unawaited(PendingUploadsBanner._openSheet(context, albumId)), child: Column( mainAxisSize: MainAxisSize.min, children: [ diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart index 379f0975b1..8edfca5bf1 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; @@ -48,7 +50,7 @@ class _LocationDetailsState extends ConsumerState { if (widget.exifInfo != oldWidget.exifInfo) { final exif = widget.exifInfo; if (exif != null && exif.hasCoordinates) { - _mapController?.moveCamera(CameraUpdate.newLatLng(LatLng(exif.latitude!, exif.longitude!))); + unawaited(_mapController?.moveCamera(CameraUpdate.newLatLng(LatLng(exif.latitude!, exif.longitude!)))); } } } diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart index 278da294c0..72db236b3c 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/people_details.widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -74,7 +76,7 @@ class PeopleDetails extends ConsumerWidget { return; } ContextHelper(context).pop(); - context.pushRoute(DriftPersonRoute(person: person)); + unawaited(context.pushRoute(DriftPersonRoute(person: person))); }, onNameTap: () => showNameEditModal(person), ), diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart index 1ef22a891f..7233356402 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart @@ -87,8 +87,8 @@ class _AssetPageState extends ConsumerState { @override void dispose() { _scrollController.dispose(); - _scaleBoundarySub?.cancel(); - _eventSubscription?.cancel(); + unawaited(_scaleBoundarySub?.cancel()); + unawaited(_eventSubscription?.cancel()); super.dispose(); } @@ -112,7 +112,7 @@ class _AssetPageState extends ConsumerState { return; } _viewer.setShowingDetails(true); - _scrollController.animateTo(_snapOffset, duration: Durations.medium2, curve: Curves.easeOutCubic); + unawaited(_scrollController.animateTo(_snapOffset, duration: Durations.medium2, curve: Curves.easeOutCubic)); } bool _willClose(double scrollVelocity) => @@ -199,7 +199,7 @@ class _AssetPageState extends ConsumerState { case _DragIntent.dismiss: const popThreshold = 75.0; if (details.localPosition.dy - start!.localPosition.dy > popThreshold) { - context.maybePop(); + unawaited(context.maybePop()); return; } _viewController?.animateMultiple( @@ -292,14 +292,14 @@ class _AssetPageState extends ConsumerState { } void _listenForScaleBoundaries(PhotoViewControllerBase? controller) { - _scaleBoundarySub?.cancel(); + unawaited(_scaleBoundarySub?.cancel()); _scaleBoundarySub = null; if (controller == null || controller.scaleBoundaries != null) { return; } _scaleBoundarySub = controller.outputStateStream.listen((_) { if (controller.scaleBoundaries != null) { - _scaleBoundarySub?.cancel(); + unawaited(_scaleBoundarySub?.cancel()); _scaleBoundarySub = null; if (mounted) { setState(() {}); diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart index 3952dafdb2..23cc6119fb 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart @@ -107,7 +107,7 @@ class _AssetViewerState extends ConsumerState { final maxPage = _totalAssets - 1; if (target >= 0 && target <= maxPage) { _pageController.jumpToPage(target); - _onAssetChanged(target); + unawaited(_onAssetChanged(target)); } } @@ -126,14 +126,14 @@ class _AssetViewerState extends ConsumerState { WidgetsBinding.instance.addPostFrameCallback(_onAssetInit); final assetViewer = ref.read(assetViewerProvider); - _setSystemUIMode(assetViewer.showingControls, assetViewer.showingDetails); + unawaited(_setSystemUIMode(assetViewer.showingControls, assetViewer.showingDetails)); } @override void dispose() { _pageController.dispose(); _preloader.dispose(); - _reloadSubscription?.cancel(); + unawaited(_reloadSubscription?.cancel()); _stackChildrenKeepAlive?.close(); unawaited(restoreEdgeToEdge()); @@ -157,7 +157,7 @@ class _AssetViewerState extends ConsumerState { final page = _pageController.page?.round(); if (page != null && page != _currentPage) { - _onAssetChanged(page); + unawaited(_onAssetChanged(page)); } return false; } @@ -223,7 +223,7 @@ class _AssetViewerState extends ConsumerState { case ViewerReloadAssetEvent(): _onViewerReloadEvent(); case final ViewerStackAssetDeletedEvent event: - _onViewerStackAssetDeletedEvent(event); + unawaited(_onViewerStackAssetDeletedEvent(event)); default: } } @@ -235,8 +235,8 @@ class _AssetViewerState extends ConsumerState { final index = _pageController.page?.round() ?? 0; final target = index >= _totalAssets - 1 ? index - 1 : index + 1; - _pageController.animateToPage(target, duration: Durations.medium1, curve: Curves.easeInOut); - _onAssetChanged(target); + unawaited(_pageController.animateToPage(target, duration: Durations.medium1, curve: Curves.easeInOut)); + unawaited(_onAssetChanged(target)); } Future _onViewerStackAssetDeletedEvent(ViewerStackAssetDeletedEvent event) async { @@ -271,7 +271,7 @@ class _AssetViewerState extends ConsumerState { final totalAssets = timelineService.totalAssets; if (totalAssets == 0) { - context.maybePop(); + unawaited(context.maybePop()); return; } @@ -281,14 +281,14 @@ class _AssetViewerState extends ConsumerState { if (index != _currentPage) { _pageController.jumpToPage(index); - _onAssetChanged(index); + unawaited(_onAssetChanged(index)); } else if (currentAsset is RemoteAsset && currentAsset.stackId != null && assetIndex == null) { final timelineAsset = timelineService.getAssetSafe(index); if (timelineAsset is! RemoteAsset || currentAsset.stackId != timelineAsset.stackId) { - _onAssetChanged(index); + unawaited(_onAssetChanged(index)); } } else if (currentAsset != null && assetIndex == null) { - _onAssetChanged(index); + unawaited(_onAssetChanged(index)); } if (_totalAssets != totalAssets) { @@ -298,9 +298,9 @@ class _AssetViewerState extends ConsumerState { } } - void _setSystemUIMode(bool controls, bool details) { + Future _setSystemUIMode(bool controls, bool details) { final immersive = !controls || (CurrentPlatform.isIOS && details); - unawaited(immersive ? SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky) : restoreEdgeToEdge()); + return immersive ? SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky) : restoreEdgeToEdge(); } @override @@ -324,7 +324,7 @@ class _AssetViewerState extends ConsumerState { ref.listen(assetViewerProvider.select((value) => (value.showingControls, value.showingDetails)), (_, state) { final (controls, details) = state; - _setSystemUIMode(controls, details); + unawaited(_setSystemUIMode(controls, details)); }); return AnnotatedRegion( diff --git a/mobile/lib/presentation/widgets/asset_viewer/ocr_overlay.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/ocr_overlay.widget.dart index a9291f3173..576d4937b6 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/ocr_overlay.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/ocr_overlay.widget.dart @@ -82,7 +82,7 @@ class _OcrOverlayState extends ConsumerState { } void _detachController() { - _controllerSub?.cancel(); + unawaited(_controllerSub?.cancel()); _controllerSub = null; } diff --git a/mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart index 69e84ee03d..ac6f79a155 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -26,7 +28,7 @@ class SheetTile extends ConsumerWidget { }); void copyTitle(BuildContext context, WidgetRef ref) { - Clipboard.setData(ClipboardData(text: title)); + unawaited(Clipboard.setData(ClipboardData(text: title))); ImmichToast.show( context: context, msg: 'copied_to_clipboard'.t(context: context), diff --git a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart index 6f2398046f..63b5663d9d 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -66,7 +66,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg if (!widget.isCurrent) { _loadTimer?.cancel(); - _notifier.pause(); + unawaited(_notifier.pause()); return; } @@ -293,7 +293,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg _controller = nc; if (widget.isCurrent) { - _loadVideo(); + unawaited(_loadVideo()); } } diff --git a/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart index 878a1c9405..9c9f8f7139 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -52,11 +54,13 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { IconButton( icon: const Icon(Icons.chat_outlined), onPressed: () { - context.router.push( - DriftActivitiesRoute( - album: album, - assetId: asset is RemoteAsset ? asset.id : null, - assetName: asset.name, + unawaited( + context.router.push( + DriftActivitiesRoute( + album: album, + assetId: asset is RemoteAsset ? asset.id : null, + assetName: asset.name, + ), ), ); }, diff --git a/mobile/lib/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart index d5ed3f6c96..a066e0167e 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -53,10 +55,12 @@ class _BaseDraggableScrollableSheetState extends ConsumerState } if (previous?.isInteracting != true && next.isInteracting) { - _controller.animateTo( - widget.minChildSize, - duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, + unawaited( + _controller.animateTo( + widget.minChildSize, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + ), ); } }); diff --git a/mobile/lib/presentation/widgets/feature_message/feature_message_dialog.widget.dart b/mobile/lib/presentation/widgets/feature_message/feature_message_dialog.widget.dart index 2c89c68d99..d748452c0e 100644 --- a/mobile/lib/presentation/widgets/feature_message/feature_message_dialog.widget.dart +++ b/mobile/lib/presentation/widgets/feature_message/feature_message_dialog.widget.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:math' as math; import 'package:easy_localization/easy_localization.dart'; @@ -60,7 +61,7 @@ class _FeatureMessageDialogState extends State<_FeatureMessageDialog> with Singl Navigator.of(context).pop(); return; } - _controller.nextPage(duration: const Duration(milliseconds: 320), curve: Curves.easeOutCubic); + unawaited(_controller.nextPage(duration: const Duration(milliseconds: 320), curve: Curves.easeOutCubic)); } List _borderColors(BuildContext context) { diff --git a/mobile/lib/presentation/widgets/images/full_image.widget.dart b/mobile/lib/presentation/widgets/images/full_image.widget.dart index 78fc0a6a21..8c92ac2818 100644 --- a/mobile/lib/presentation/widgets/images/full_image.widget.dart +++ b/mobile/lib/presentation/widgets/images/full_image.widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; @@ -30,7 +32,7 @@ class FullImage extends StatelessWidget { height: size.height, fit: fit, errorBuilder: (context, error, stackTrace) { - provider.evict(); + unawaited(provider.evict()); return const Icon(Icons.image_not_supported_outlined, size: 32); }, ); diff --git a/mobile/lib/presentation/widgets/images/image_provider.dart b/mobile/lib/presentation/widgets/images/image_provider.dart index 9cc386e302..927734ca25 100644 --- a/mobile/lib/presentation/widgets/images/image_provider.dart +++ b/mobile/lib/presentation/widgets/images/image_provider.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'dart:ui' as ui; @@ -43,10 +44,12 @@ mixin CancellableImageProviderMixin on CancellableImageProvide return cachedImage; } - completer.operation.valueOrCancellation().whenComplete(() { - cachedStream.removeListener(listener); - cachedOperation = null; - }); + unawaited( + completer.operation.valueOrCancellation().whenComplete(() { + cachedStream.removeListener(listener); + cachedOperation = null; + }), + ); cachedOperation = completer.operation; return null; } @@ -138,7 +141,7 @@ mixin CancellableImageProviderMixin on CancellableImageProvide final operation = cachedOperation; if (operation != null) { cachedOperation = null; - operation.cancel(); + unawaited(operation.cancel()); } if (hasActiveWork) { diff --git a/mobile/lib/presentation/widgets/images/thumbnail.widget.dart b/mobile/lib/presentation/widgets/images/thumbnail.widget.dart index 847fa4e381..90bb79cced 100644 --- a/mobile/lib/presentation/widgets/images/thumbnail.widget.dart +++ b/mobile/lib/presentation/widgets/images/thumbnail.widget.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; @@ -130,9 +131,9 @@ class _ThumbnailState extends State with SingleTickerProviderStateMix if ((synchronousCall && _providerImage == null) || !_isVisible()) { _fadeController.value = 1.0; } else if (_fadeController.isAnimating) { - _fadeController.forward(); + unawaited(_fadeController.forward()); } else { - _fadeController.forward(from: 0.0); + unawaited(_fadeController.forward(from: 0.0)); } setState(() { diff --git a/mobile/lib/presentation/widgets/map/map.state.dart b/mobile/lib/presentation/widgets/map/map.state.dart index f1b4f80ec1..eedf3aadf5 100644 --- a/mobile/lib/presentation/widgets/map/map.state.dart +++ b/mobile/lib/presentation/widgets/map/map.state.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/events.model.dart'; @@ -87,32 +89,32 @@ class MapStateNotifier extends Notifier { } void switchFavoriteOnly(bool isFavoriteOnly) { - ref.read(settingsProvider).write(.mapShowFavoriteOnly, isFavoriteOnly); + unawaited(ref.read(settingsProvider).write(.mapShowFavoriteOnly, isFavoriteOnly)); state = state.copyWith(onlyFavorites: isFavoriteOnly); EventStream.shared.emit(const MapMarkerReloadEvent()); } void switchIncludeArchived(bool isIncludeArchived) { - ref.read(settingsProvider).write(.mapIncludeArchived, isIncludeArchived); + unawaited(ref.read(settingsProvider).write(.mapIncludeArchived, isIncludeArchived)); state = state.copyWith(includeArchived: isIncludeArchived); EventStream.shared.emit(const MapMarkerReloadEvent()); } void switchWithPartners(bool isWithPartners) { - ref.read(settingsProvider).write(.mapWithPartners, isWithPartners); + unawaited(ref.read(settingsProvider).write(.mapWithPartners, isWithPartners)); state = state.copyWith(withPartners: isWithPartners); EventStream.shared.emit(const MapMarkerReloadEvent()); } void setRelativeTime(int relativeDays) { - ref.read(settingsProvider).write(.mapRelativeDate, relativeDays); + unawaited(ref.read(settingsProvider).write(.mapRelativeDate, relativeDays)); state = state.copyWith(relativeDays: relativeDays); EventStream.shared.emit(const MapMarkerReloadEvent()); } void setCustomTimeRange(TimeRange range) { - ref.read(settingsProvider).write(.mapCustomFrom, range.from); - ref.read(settingsProvider).write(.mapCustomTo, range.to); + unawaited(ref.read(settingsProvider).write(.mapCustomFrom, range.from)); + unawaited(ref.read(settingsProvider).write(.mapCustomTo, range.to)); state = state.copyWith(timeRange: range); EventStream.shared.emit(const MapMarkerReloadEvent()); } diff --git a/mobile/lib/presentation/widgets/map/map.widget.dart b/mobile/lib/presentation/widgets/map/map.widget.dart index a68a475ad4..918d64054b 100644 --- a/mobile/lib/presentation/widgets/map/map.widget.dart +++ b/mobile/lib/presentation/widgets/map/map.widget.dart @@ -66,7 +66,7 @@ class _DriftMapState extends ConsumerState { void dispose() { _debouncer.dispose(); bottomSheetOffset.dispose(); - _eventSubscription?.cancel(); + unawaited(_eventSubscription?.cancel()); super.dispose(); } diff --git a/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart b/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart index 62889b10cb..b0b7816889 100644 --- a/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart +++ b/mobile/lib/presentation/widgets/memory/memory_lane.widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -33,7 +35,7 @@ class DriftMemoryLane extends ConsumerWidget { if (memories[index].assets.isNotEmpty) { DriftMemoryPage.setMemory(ref, memories[index]); } - context.pushRoute(DriftMemoryRoute(memories: memories, memoryIndex: index)); + unawaited(context.pushRoute(DriftMemoryRoute(memories: memories, memoryIndex: index))); }, children: memories .map((memory) => DriftMemoryCard(key: Key(memory.id), memory: memory)) diff --git a/mobile/lib/presentation/widgets/timeline/header.widget.dart b/mobile/lib/presentation/widgets/timeline/header.widget.dart index d73d024efb..76176041fa 100644 --- a/mobile/lib/presentation/widgets/timeline/header.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/header.widget.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -105,7 +107,7 @@ class _BulkSelectIconButton extends ConsumerWidget { ? const SizedBox.shrink() : IconButton( onPressed: () { - ref.read(multiSelectProvider.notifier).toggleBucketSelection(assetOffset, bucket.assetCount); + unawaited(ref.read(multiSelectProvider.notifier).toggleBucketSelection(assetOffset, bucket.assetCount)); ref.read(hapticFeedbackProvider.notifier).heavyImpact(); }, icon: isAllSelected diff --git a/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart b/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart index f5e3493a8e..eb081a1e6a 100644 --- a/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/scrubber.widget.dart @@ -152,7 +152,7 @@ class ScrubberState extends ConsumerState with TickerProviderStateMixi void _resetThumbTimer() { _fadeOutTimer?.cancel(); _fadeOutTimer = Timer(kTimelineScrubberFadeOutDuration, () { - _thumbAnimationController.reverse(); + unawaited(_thumbAnimationController.reverse()); _fadeOutTimer = null; }); } @@ -177,10 +177,10 @@ class ScrubberState extends ConsumerState with TickerProviderStateMixi if (notification is ScrollUpdateNotification) { _thumbTopOffset = _currentOffset; if (_labelAnimation.status != AnimationStatus.reverse) { - _labelAnimationController.reverse(); + unawaited(_labelAnimationController.reverse()); } if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); + unawaited(_thumbAnimationController.forward()); } } _resetThumbTimer(); @@ -210,7 +210,7 @@ class ScrubberState extends ConsumerState with TickerProviderStateMixi void _onDragStart(DragStartDetails _) { setState(() { _isDragging = true; - _labelAnimationController.forward(); + unawaited(_labelAnimationController.forward()); _fadeOutTimer?.cancel(); _lastLabel = null; }); @@ -226,7 +226,7 @@ class ScrubberState extends ConsumerState with TickerProviderStateMixi } if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); + unawaited(_thumbAnimationController.forward()); } final dragPosition = _calculateDragPosition(details); @@ -344,7 +344,7 @@ class ScrubberState extends ConsumerState with TickerProviderStateMixi } void _onDragEnd(DragEndDetails _) { - _labelAnimationController.reverse(); + unawaited(_labelAnimationController.reverse()); setState(() { _isDragging = false; }); diff --git a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart index 5bd39deb8a..9e65dcb72c 100644 --- a/mobile/lib/presentation/widgets/timeline/timeline.widget.dart +++ b/mobile/lib/presentation/widgets/timeline/timeline.widget.dart @@ -258,7 +258,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBi void dispose() { WidgetsBinding.instance.removeObserver(this); _scrollController.dispose(); - _eventSubscription?.cancel(); + unawaited(_eventSubscription?.cancel()); super.dispose(); } @@ -269,9 +269,11 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBi final timelineState = ref.read(timelineStateProvider.notifier); timelineState.setScrubbing(true); - _scrollController - .animateTo(0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut) - .whenComplete(() => timelineState.setScrubbing(false)); + unawaited( + _scrollController + .animateTo(0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut) + .whenComplete(() => timelineState.setScrubbing(false)), + ); } void _scrollToDate(DateTime date) { @@ -303,13 +305,15 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBi // Scroll to the segment with a small offset to show the header final targetOffset = fallbackSegment.startOffset - 50; timelineState.setScrubbing(true); - _scrollController - .animateTo( - targetOffset.clamp(0.0, _scrollController.position.maxScrollExtent), - duration: const Duration(milliseconds: 500), - curve: Curves.easeInOut, - ) - .whenComplete(() => timelineState.setScrubbing(false)); + unawaited( + _scrollController + .animateTo( + targetOffset.clamp(0.0, _scrollController.position.maxScrollExtent), + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ) + .whenComplete(() => timelineState.setScrubbing(false)), + ); } else { timelineState.setScrubbing(false); } @@ -344,8 +348,8 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBi }); } - void _dragScroll(ScrollDirection direction) { - _scrollController.animateTo( + Future _dragScroll(ScrollDirection direction) { + return _scrollController.animateTo( _scrollController.offset + (direction == ScrollDirection.forward ? 175 : -175), duration: const Duration(milliseconds: 125), curve: Curves.easeOut, @@ -488,7 +492,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBi _restoreAssetIndex = targetAssetIndex; }); - ref.read(settingsProvider).write(.timelineTilesPerRow, _perRow); + unawaited(ref.read(settingsProvider).write(.timelineTilesPerRow, _perRow)); } }; }, @@ -498,7 +502,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> with WidgetsBi onStart: !isReadonlyModeEnabled ? _setDragStartIndex : null, onAssetEnter: _handleDragAssetEnter, onEnd: !isReadonlyModeEnabled ? _stopDrag : null, - onScroll: _dragScroll, + onScroll: (direction) => unawaited(_dragScroll(direction)), onScrollStart: () { // Minimize the bottom sheet when drag selection starts ref.read(timelineStateProvider.notifier).setScrolling(true); diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 2b52973c0a..8678f7c32e 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -120,7 +120,7 @@ class AppLifeCycleNotifier extends StateNotifier { if (syncSuccess) { await Future.wait([ _safeRun(backgroundManager.hashAssets(), "hashAssets").then((_) { - _resumeBackup(); + unawaited(_resumeBackup()); }), _resumeBackup(), // TODO: Bring back when the soft freeze issue is addressed diff --git a/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart b/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart index 6808860ffc..7b1d5d2caa 100644 --- a/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart +++ b/mobile/lib/providers/asset_viewer/asset_viewer.provider.dart @@ -88,7 +88,7 @@ class AssetViewerStateNotifier extends Notifier { } void reset() { - _assetSubscription?.cancel(); + unawaited(_assetSubscription?.cancel()); _assetSubscription = null; state = const AssetViewerState(); } @@ -102,7 +102,7 @@ class AssetViewerStateNotifier extends Notifier { } void _watchCurrentAsset(BaseAsset asset) { - _assetSubscription?.cancel(); + unawaited(_assetSubscription?.cancel()); _assetSubscription = ref.read(assetServiceProvider).watchAsset(asset).listen((updated) { if (updated != null) { state = state.copyWith(currentAsset: updated); diff --git a/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart b/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart index 51119f4ba2..47d67c9674 100644 --- a/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart +++ b/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -33,7 +34,7 @@ class ShareIntentUploadStateNotifier extends StateNotifier route.name == "ShareIntentRoute"); clearAttachments(); addAttachments(attachments); - router.push(ShareIntentRoute(attachments: attachments)); + unawaited(router.push(ShareIntentRoute(attachments: attachments))); } void addAttachments(List attachments) { diff --git a/mobile/lib/providers/asset_viewer/video_player_provider.dart b/mobile/lib/providers/asset_viewer/video_player_provider.dart index 463a1ac3d2..74d697a2ea 100644 --- a/mobile/lib/providers/asset_viewer/video_player_provider.dart +++ b/mobile/lib/providers/asset_viewer/video_player_provider.dart @@ -50,7 +50,7 @@ class VideoPlayerNotifier extends StateNotifier { void dispose() { _bufferingTimer?.cancel(); _seekTimer?.cancel(); - WakelockPlus.disable(); + unawaited(WakelockPlus.disable()); _controller = null; super.dispose(); @@ -121,7 +121,7 @@ class VideoPlayerNotifier extends StateNotifier { } _seekTimer = Timer(const Duration(milliseconds: 150), () { - _controller?.seekTo(state.position.inMilliseconds); + unawaited(_controller?.seekTo(state.position.inMilliseconds)); }); } @@ -130,11 +130,11 @@ class VideoPlayerNotifier extends StateNotifier { switch (state.status) { case VideoPlaybackStatus.paused: - play(); + unawaited(play()); case VideoPlaybackStatus.playing || VideoPlaybackStatus.buffering: - pause(); + unawaited(pause()); case VideoPlaybackStatus.completed: - restart(); + unawaited(restart()); } } @@ -145,7 +145,7 @@ class VideoPlayerNotifier extends StateNotifier { } _holdStatus = state.status; - pause(); + unawaited(pause()); } /// Restores playback to the status before [hold] was called. @@ -155,7 +155,7 @@ class VideoPlayerNotifier extends StateNotifier { switch (status) { case VideoPlaybackStatus.playing || VideoPlaybackStatus.buffering: - play(); + unawaited(play()); default: } } @@ -238,7 +238,7 @@ class VideoPlayerNotifier extends StateNotifier { final newStatus = _mapStatus(playbackInfo.status); switch (newStatus) { case VideoPlaybackStatus.playing: - WakelockPlus.enable(); + unawaited(WakelockPlus.enable()); _startBufferingTimer(); default: onNativePlaybackEnded(); @@ -250,7 +250,7 @@ class VideoPlayerNotifier extends StateNotifier { } void onNativePlaybackEnded() { - WakelockPlus.disable(); + unawaited(WakelockPlus.disable()); _bufferingTimer?.cancel(); } diff --git a/mobile/lib/providers/backup/backup_album.provider.dart b/mobile/lib/providers/backup/backup_album.provider.dart index f81f905c2f..25a4204928 100644 --- a/mobile/lib/providers/backup/backup_album.provider.dart +++ b/mobile/lib/providers/backup/backup_album.provider.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/services/local_album.service.dart'; @@ -10,7 +12,7 @@ final backupAlbumProvider = StateNotifierProvider> { BackupAlbumNotifier(this._localAlbumService) : super([]) { - getAll(); + unawaited(getAll()); } final LocalAlbumService _localAlbumService; diff --git a/mobile/lib/providers/cast.provider.dart b/mobile/lib/providers/cast.provider.dart index 943ac930ec..776888146b 100644 --- a/mobile/lib/providers/cast.provider.dart +++ b/mobile/lib/providers/cast.provider.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/models/cast/cast_manager_state.dart'; @@ -51,7 +53,7 @@ class CastNotifier extends StateNotifier { } void loadMedia(RemoteAsset asset, bool reload) { - _gCastService.loadMedia(asset, reload); + unawaited(_gCastService.loadMedia(asset, reload)); } Future connect(CastDestinationType type, dynamic device) async { diff --git a/mobile/lib/providers/cleanup.provider.dart b/mobile/lib/providers/cleanup.provider.dart index 378ceb010f..4316b4eb00 100644 --- a/mobile/lib/providers/cleanup.provider.dart +++ b/mobile/lib/providers/cleanup.provider.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + 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'; @@ -87,18 +89,18 @@ class CleanupNotifier extends StateNotifier { state = state.copyWith(selectedDate: date, assetsToDelete: []); if (date != null) { final daysAgo = DateTime.now().difference(date).inDays; - _settingsRepository.write(.cleanupCutoffDaysAgo, daysAgo); + unawaited(_settingsRepository.write(.cleanupCutoffDaysAgo, daysAgo)); } } void setKeepMediaType(AssetKeepType keepMediaType) { state = state.copyWith(keepMediaType: keepMediaType, assetsToDelete: []); - _settingsRepository.write(.cleanupKeepMediaType, keepMediaType); + unawaited(_settingsRepository.write(.cleanupKeepMediaType, keepMediaType)); } void setKeepFavorites(bool keepFavorites) { state = state.copyWith(keepFavorites: keepFavorites, assetsToDelete: []); - _settingsRepository.write(.cleanupKeepFavorites, keepFavorites); + unawaited(_settingsRepository.write(.cleanupKeepFavorites, keepFavorites)); } void toggleKeepAlbum(String albumId) { @@ -118,7 +120,7 @@ class CleanupNotifier extends StateNotifier { } void _persistExcludedAlbumIds(Set albumIds) { - _settingsRepository.write(.cleanupKeepAlbumIds, albumIds.toList()); + unawaited(_settingsRepository.write(.cleanupKeepAlbumIds, albumIds.toList())); } void cleanupStaleAlbumIds(Set existingAlbumIds) { @@ -144,7 +146,7 @@ class CleanupNotifier extends StateNotifier { _persistExcludedAlbumIds(keepAlbumIds); } - _settingsRepository.write(.cleanupDefaultsInitialized, true); + unawaited(_settingsRepository.write(.cleanupDefaultsInitialized, true)); } Future scanAssets() async { diff --git a/mobile/lib/providers/gallery_permission.provider.dart b/mobile/lib/providers/gallery_permission.provider.dart index 315c67a214..6d4703c834 100644 --- a/mobile/lib/providers/gallery_permission.provider.dart +++ b/mobile/lib/providers/gallery_permission.provider.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; @@ -9,7 +10,7 @@ class GalleryPermissionNotifier extends StateNotifier { : super(PermissionStatus.denied) // Denied is the initial state { // Sets the initial state - getGalleryPermissionStatus(); + unawaited(getGalleryPermissionStatus()); } bool get hasPermission => state.isGranted || state.isLimited; diff --git a/mobile/lib/providers/haptic_feedback.provider.dart b/mobile/lib/providers/haptic_feedback.provider.dart index e1ce5c8d0d..850935163d 100644 --- a/mobile/lib/providers/haptic_feedback.provider.dart +++ b/mobile/lib/providers/haptic_feedback.provider.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; @@ -15,31 +17,31 @@ class HapticNotifier extends StateNotifier { void selectionClick() { if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { - HapticFeedback.selectionClick(); + unawaited(HapticFeedback.selectionClick()); } } void lightImpact() { if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { - HapticFeedback.lightImpact(); + unawaited(HapticFeedback.lightImpact()); } } void mediumImpact() { if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { - HapticFeedback.mediumImpact(); + unawaited(HapticFeedback.mediumImpact()); } } void heavyImpact() { if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { - HapticFeedback.heavyImpact(); + unawaited(HapticFeedback.heavyImpact()); } } void vibrate() { if (_ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableHapticFeedback)) { - HapticFeedback.vibrate(); + unawaited(HapticFeedback.vibrate()); } } } diff --git a/mobile/lib/providers/infrastructure/readonly_mode.provider.dart b/mobile/lib/providers/infrastructure/readonly_mode.provider.dart index d503919c90..be94a8a341 100644 --- a/mobile/lib/providers/infrastructure/readonly_mode.provider.dart +++ b/mobile/lib/providers/infrastructure/readonly_mode.provider.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; @@ -16,11 +18,11 @@ class ReadOnlyModeNotifier extends Notifier { void setMode(bool value) { final isLoggedIn = ref.read(authProvider).isAuthenticated; - _appSettingService.setSetting(AppSettingsEnum.readonlyModeEnabled, value); + unawaited(_appSettingService.setSetting(AppSettingsEnum.readonlyModeEnabled, value)); state = value; if (value && isLoggedIn) { - ref.read(appRouterProvider).navigate(const MainTimelineRoute()); + unawaited(ref.read(appRouterProvider).navigate(const MainTimelineRoute())); } } diff --git a/mobile/lib/providers/local_auth.provider.dart b/mobile/lib/providers/local_auth.provider.dart index d2860975bb..58bd4fb0f8 100644 --- a/mobile/lib/providers/local_auth.provider.dart +++ b/mobile/lib/providers/local_auth.provider.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -21,9 +23,11 @@ class LocalAuthNotifier extends StateNotifier { LocalAuthNotifier(this._localAuthService, this._secureStorageService) : super(const BiometricStatus(availableBiometrics: [], canAuthenticate: false)) { - _localAuthService.getStatus().then((value) { - state = state.copyWith(canAuthenticate: value.canAuthenticate, availableBiometrics: value.availableBiometrics); - }); + unawaited( + _localAuthService.getStatus().then((value) { + state = state.copyWith(canAuthenticate: value.canAuthenticate, availableBiometrics: value.availableBiometrics); + }), + ); } Future registerBiometric(BuildContext context, String pinCode) async { diff --git a/mobile/lib/providers/map/map_state.provider.dart b/mobile/lib/providers/map/map_state.provider.dart index b643264dca..12e9e59fac 100644 --- a/mobile/lib/providers/map/map_state.provider.dart +++ b/mobile/lib/providers/map/map_state.provider.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/models/map/map_state.model.dart'; @@ -26,12 +28,12 @@ class MapStateNotifier extends Notifier { } void switchTheme(ThemeMode mode) { - ref.read(settingsProvider).write(.mapThemeMode, mode); + unawaited(ref.read(settingsProvider).write(.mapThemeMode, mode)); state = state.copyWith(themeMode: mode); } void switchFavoriteOnly(bool isFavoriteOnly) { - ref.read(settingsProvider).write(.mapShowFavoriteOnly, isFavoriteOnly); + unawaited(ref.read(settingsProvider).write(.mapShowFavoriteOnly, isFavoriteOnly)); state = state.copyWith(showFavoriteOnly: isFavoriteOnly, shouldRefetchMarkers: true); } @@ -40,17 +42,17 @@ class MapStateNotifier extends Notifier { } void switchIncludeArchived(bool isIncludeArchived) { - ref.read(settingsProvider).write(.mapIncludeArchived, isIncludeArchived); + unawaited(ref.read(settingsProvider).write(.mapIncludeArchived, isIncludeArchived)); state = state.copyWith(includeArchived: isIncludeArchived, shouldRefetchMarkers: true); } void switchWithPartners(bool isWithPartners) { - ref.read(settingsProvider).write(.mapWithPartners, isWithPartners); + unawaited(ref.read(settingsProvider).write(.mapWithPartners, isWithPartners)); state = state.copyWith(withPartners: isWithPartners, shouldRefetchMarkers: true); } void setRelativeTime(int relativeTime) { - ref.read(settingsProvider).write(.mapRelativeDate, relativeTime); + unawaited(ref.read(settingsProvider).write(.mapRelativeDate, relativeTime)); state = state.copyWith(relativeTime: relativeTime, shouldRefetchMarkers: true); } } diff --git a/mobile/lib/providers/permission.provider.dart b/mobile/lib/providers/permission.provider.dart index b7011e1357..dc82285122 100644 --- a/mobile/lib/providers/permission.provider.dart +++ b/mobile/lib/providers/permission.provider.dart @@ -10,7 +10,7 @@ class NotificationPermissionNotifier extends StateNotifier { NotificationPermissionNotifier() : super(Platform.isAndroid ? PermissionStatus.granted : PermissionStatus.restricted) { // Sets the initial state - getNotificationPermission().then((p) => state = p); + unawaited(getNotificationPermission().then((p) => state = p)); } /// Requests the notification permission diff --git a/mobile/lib/providers/server_info.provider.dart b/mobile/lib/providers/server_info.provider.dart index bf83b36f54..c25e496a04 100644 --- a/mobile/lib/providers/server_info.provider.dart +++ b/mobile/lib/providers/server_info.provider.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/models/server_info/server_config.model.dart'; @@ -77,7 +79,7 @@ class ServerInfoNotifier extends StateNotifier { void handleReleaseInfo(ServerVersion serverVersion, ServerVersion? latestVersion) { // Update local server version - _checkServerVersionMismatch(serverVersion, latestVersion: latestVersion); + unawaited(_checkServerVersionMismatch(serverVersion, latestVersion: latestVersion)); } Future getServerFeatures() async { diff --git a/mobile/lib/providers/shared_link.provider.dart b/mobile/lib/providers/shared_link.provider.dart index fb44aea203..096919f28a 100644 --- a/mobile/lib/providers/shared_link.provider.dart +++ b/mobile/lib/providers/shared_link.provider.dart @@ -8,7 +8,7 @@ class SharedLinksNotifier extends StateNotifier>> { final SharedLinkService _sharedLinkService; SharedLinksNotifier(this._sharedLinkService) : super(const AsyncLoading()) { - fetchLinks(); + unawaited(fetchLinks()); } Future fetchLinks() async { diff --git a/mobile/lib/providers/user.provider.dart b/mobile/lib/providers/user.provider.dart index 622847b0c2..2feb39ce5c 100644 --- a/mobile/lib/providers/user.provider.dart +++ b/mobile/lib/providers/user.provider.dart @@ -22,7 +22,7 @@ class CurrentUserProvider extends StateNotifier { @override void dispose() { - streamSub.cancel(); + unawaited(streamSub.cancel()); super.dispose(); } } diff --git a/mobile/lib/providers/websocket.provider.dart b/mobile/lib/providers/websocket.provider.dart index a7c08457af..2eb8ddc2b4 100644 --- a/mobile/lib/providers/websocket.provider.dart +++ b/mobile/lib/providers/websocket.provider.dart @@ -143,8 +143,8 @@ class WebsocketNotifier extends StateNotifier { } void _handleOnConfigUpdate(dynamic _) { - _ref.read(serverInfoProvider.notifier).getServerFeatures(); - _ref.read(serverInfoProvider.notifier).getServerConfig(); + unawaited(_ref.read(serverInfoProvider.notifier).getServerFeatures()); + unawaited(_ref.read(serverInfoProvider.notifier).getServerConfig()); } void _handleReleaseUpdates(dynamic data) { @@ -203,7 +203,7 @@ class WebsocketNotifier extends StateNotifier { unawaited( _ref.read(backgroundSyncProvider).syncWebsocketBatchV1(_batchedAssetUploadReady.toList()).then((_) { if (isSyncAlbumEnabled) { - _ref.read(backgroundSyncProvider).syncLinkedAlbum(); + unawaited(_ref.read(backgroundSyncProvider).syncLinkedAlbum()); } }), ); @@ -224,7 +224,7 @@ class WebsocketNotifier extends StateNotifier { unawaited( _ref.read(backgroundSyncProvider).syncWebsocketBatchV2(_batchedAssetUploadReady.toList()).then((_) { if (isSyncAlbumEnabled) { - _ref.read(backgroundSyncProvider).syncLinkedAlbum(); + unawaited(_ref.read(backgroundSyncProvider).syncLinkedAlbum()); } }), ); diff --git a/mobile/lib/routing/app_navigation_observer.dart b/mobile/lib/routing/app_navigation_observer.dart index 57304af44f..f126788008 100644 --- a/mobile/lib/routing/app_navigation_observer.dart +++ b/mobile/lib/routing/app_navigation_observer.dart @@ -13,10 +13,12 @@ class AppNavigationObserver extends AutoRouterObserver { @override void didPush(Route route, Route? previousRoute) { - Future(() { - ref.read(currentRouteNameProvider.notifier).state = route.settings.name; - ref.read(previousRouteNameProvider.notifier).state = previousRoute?.settings.name; - ref.read(previousRouteDataProvider.notifier).state = previousRoute?.settings; - }); + unawaited( + Future(() { + ref.read(currentRouteNameProvider.notifier).state = route.settings.name; + ref.read(previousRouteNameProvider.notifier).state = previousRoute?.settings.name; + ref.read(previousRouteDataProvider.notifier).state = previousRoute?.settings; + }), + ); } } diff --git a/mobile/lib/services/background_upload.service.dart b/mobile/lib/services/background_upload.service.dart index 5b379ff890..5312107f6c 100644 --- a/mobile/lib/services/background_upload.service.dart +++ b/mobile/lib/services/background_upload.service.dart @@ -135,12 +135,12 @@ class BackgroundUploadService { if (!_taskStatusController.isClosed) { _taskStatusController.add(update); } - _handleTaskStatusUpdate(update); + unawaited(_handleTaskStatusUpdate(update)); } void dispose() { - _taskStatusController.close(); - _taskProgressController.close(); + unawaited(_taskStatusController.close()); + unawaited(_taskProgressController.close()); } /// Enqueue tasks to the background upload queue diff --git a/mobile/lib/services/map.service.dart b/mobile/lib/services/map.service.dart index 5b50e8a890..b439af8668 100644 --- a/mobile/lib/services/map.service.dart +++ b/mobile/lib/services/map.service.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:immich_mobile/mixins/error_logger.mixin.dart'; import 'package:immich_mobile/models/map/map_marker.model.dart'; import 'package:immich_mobile/services/api.service.dart'; @@ -11,7 +13,7 @@ class MapService with ErrorLoggerMixin { final logger = Logger("MapService"); MapService(this._apiService) { - _setMapUserAgentHeader(); + unawaited(_setMapUserAgentHeader()); } Future _setMapUserAgentHeader() async { diff --git a/mobile/lib/services/share_intent_service.dart b/mobile/lib/services/share_intent_service.dart index fca5c4a188..a5ab5d8bc9 100644 --- a/mobile/lib/services/share_intent_service.dart +++ b/mobile/lib/services/share_intent_service.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; import 'package:immich_mobile/repositories/share_handler.repository.dart'; @@ -12,6 +14,6 @@ class ShareIntentService { void init() { shareHandlerRepository.onSharedMedia = onSharedMedia; - shareHandlerRepository.init(); + unawaited(shareHandlerRepository.init()); } } diff --git a/mobile/lib/utils/async_mutex.dart b/mobile/lib/utils/async_mutex.dart index b97ab9b052..6c54c1220d 100644 --- a/mobile/lib/utils/async_mutex.dart +++ b/mobile/lib/utils/async_mutex.dart @@ -12,10 +12,12 @@ class AsyncMutex { Future run(Future Function() operation) { final completer = Completer(); _enqueued++; - _running.whenComplete(() { - _enqueued--; - completer.complete(Future.sync(operation)); - }); + unawaited( + _running.whenComplete(() { + _enqueued--; + completer.complete(Future.sync(operation)); + }), + ); return _running = completer.future; } } diff --git a/mobile/lib/utils/hooks/app_settings_update_hook.dart b/mobile/lib/utils/hooks/app_settings_update_hook.dart index c498b60b06..9d7cc4b064 100644 --- a/mobile/lib/utils/hooks/app_settings_update_hook.dart +++ b/mobile/lib/utils/hooks/app_settings_update_hook.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/cupertino.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:immich_mobile/entities/store.entity.dart'; @@ -7,7 +9,7 @@ ValueNotifier useAppSettingsState(AppSettingsEnum key) { final notifier = useState(Store.get(key.storeKey, key.defaultValue)); // Listen to changes to the notifier and update app settings - useValueChanged(notifier.value, (_, __) => Store.put(key.storeKey, notifier.value)); + useValueChanged(notifier.value, (_, __) => unawaited(Store.put(key.storeKey, notifier.value))); return notifier; } diff --git a/mobile/lib/utils/image_converter.dart b/mobile/lib/utils/image_converter.dart index d0fd4f873f..1a3a00130a 100644 --- a/mobile/lib/utils/image_converter.dart +++ b/mobile/lib/utils/image_converter.dart @@ -15,13 +15,15 @@ Future imageToUint8List(Image image) async { .resolve(ImageConfiguration.empty) .addListener( ImageStreamListener((ImageInfo info, bool _) { - info.image.toByteData(format: ImageByteFormat.png).then((byteData) { - if (byteData != null) { - completer.complete(byteData.buffer.asUint8List()); - } else { - completer.completeError('Failed to convert image to bytes'); - } - }); + unawaited( + info.image.toByteData(format: ImageByteFormat.png).then((byteData) { + if (byteData != null) { + completer.complete(byteData.buffer.asUint8List()); + } else { + completer.completeError('Failed to convert image to bytes'); + } + }), + ); }, onError: (exception, stackTrace) => completer.completeError(exception)), ); return completer.future; diff --git a/mobile/lib/widgets/asset_viewer/animated_play_pause.dart b/mobile/lib/widgets/asset_viewer/animated_play_pause.dart index 4be7f49b5a..bad8a6345c 100644 --- a/mobile/lib/widgets/asset_viewer/animated_play_pause.dart +++ b/mobile/lib/widgets/asset_viewer/animated_play_pause.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:ui'; import 'package:flutter/material.dart'; @@ -27,9 +28,9 @@ class AnimatedPlayPauseState extends State with SingleTickerP super.didUpdateWidget(oldWidget); if (widget.playing != oldWidget.playing) { if (widget.playing) { - animationController.forward(); + unawaited(animationController.forward()); } else { - animationController.reverse(); + unawaited(animationController.reverse()); } } } diff --git a/mobile/lib/widgets/backup/drift_album_info_list_tile.dart b/mobile/lib/widgets/backup/drift_album_info_list_tile.dart index 85f655ec86..999b64e9de 100644 --- a/mobile/lib/widgets/backup/drift_album_info_list_tile.dart +++ b/mobile/lib/widgets/backup/drift_album_info_list_tile.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; @@ -54,7 +56,7 @@ class DriftAlbumInfoListTile extends HookConsumerWidget { ref.watch(hapticFeedbackProvider.notifier).selectionClick(); if (isExcluded) { - ref.read(backupAlbumProvider.notifier).deselectAlbum(album); + unawaited(ref.read(backupAlbumProvider.notifier).deselectAlbum(album)); } else { if (album.id == 'isAll' || album.name == 'Recents') { ImmichToast.show( @@ -66,7 +68,7 @@ class DriftAlbumInfoListTile extends HookConsumerWidget { return; } - ref.read(backupAlbumProvider.notifier).excludeAlbum(album); + unawaited(ref.read(backupAlbumProvider.notifier).excludeAlbum(album)); } }, child: ListTile( @@ -75,9 +77,9 @@ class DriftAlbumInfoListTile extends HookConsumerWidget { onTap: () { ref.read(hapticFeedbackProvider.notifier).selectionClick(); if (isSelected) { - ref.read(backupAlbumProvider.notifier).deselectAlbum(album); + unawaited(ref.read(backupAlbumProvider.notifier).deselectAlbum(album)); } else { - ref.read(backupAlbumProvider.notifier).selectAlbum(album); + unawaited(ref.read(backupAlbumProvider.notifier).selectAlbum(album)); } }, leading: buildIcon(), @@ -85,7 +87,7 @@ class DriftAlbumInfoListTile extends HookConsumerWidget { subtitle: buildSubtitle(), trailing: IconButton( onPressed: () { - context.pushRoute(LocalTimelineRoute(album: album)); + unawaited(context.pushRoute(LocalTimelineRoute(album: album))); }, icon: Icon(Icons.image_outlined, color: context.primaryColor, size: 24), splashRadius: 25, diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart index 22c860becf..085f4e2120 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart @@ -38,8 +38,8 @@ class ImmichAppBarDialog extends HookConsumerWidget { final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); useEffect(() { - ref.read(backupProvider.notifier).updateDiskInfo(); - ref.read(currentUserProvider.notifier).refresh(); + unawaited(ref.read(backupProvider.notifier).updateDiskInfo()); + unawaited(ref.read(currentUserProvider.notifier).refresh()); return null; }, []); @@ -180,7 +180,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { InkWell( onTap: () { ContextHelper(context).pop(); - launchUrl(Uri.parse('https://docs.immich.app'), mode: LaunchMode.externalApplication); + unawaited(launchUrl(Uri.parse('https://docs.immich.app'), mode: LaunchMode.externalApplication)); }, child: Text("documentation", style: context.textTheme.bodySmall).tr(), ), @@ -188,7 +188,9 @@ class ImmichAppBarDialog extends HookConsumerWidget { InkWell( onTap: () { ContextHelper(context).pop(); - launchUrl(Uri.parse('https://github.com/immich-app/immich'), mode: LaunchMode.externalApplication); + unawaited( + launchUrl(Uri.parse('https://github.com/immich-app/immich'), mode: LaunchMode.externalApplication), + ); }, child: Text("profile_drawer_github", style: context.textTheme.bodySmall).tr(), ), diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart index fbec03bbbd..348e3aa14c 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; @@ -31,7 +33,7 @@ class AppBarServerInfo extends HookConsumerWidget { } useEffect(() { - getPackageInfo(); + unawaited(getPackageInfo()); return null; }, []); @@ -87,7 +89,7 @@ class _ServerInfoItem extends StatelessWidget { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - if (icon != null) ...[icon as Widget, const SizedBox(width: 8)], + if (icon != null) ...[icon! as Widget, const SizedBox(width: 8)], Text( label, style: TextStyle( diff --git a/mobile/lib/widgets/common/app_bar_dialog/server_update_notification.dart b/mobile/lib/widgets/common/app_bar_dialog/server_update_notification.dart index c29475351e..806ac87c19 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/server_update_notification.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/server_update_notification.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; @@ -20,7 +21,7 @@ class ServerUpdateNotification extends HookConsumerWidget { final Color infoColor = context.isDarkTheme ? context.primaryColor.withAlpha(55) : context.primaryColor.withAlpha(25); - void openUpdateLink() { + Future openUpdateLink() { String url; if (serverInfoState.versionStatus == VersionStatus.serverOutOfDate) { url = kImmichLatestRelease; @@ -35,7 +36,7 @@ class ServerUpdateNotification extends HookConsumerWidget { } } - launchUrlString(url, mode: LaunchMode.externalApplication); + return launchUrlString(url, mode: LaunchMode.externalApplication); } return SizedBox( @@ -68,7 +69,7 @@ class ServerUpdateNotification extends HookConsumerWidget { serverInfoState.versionStatus == VersionStatus.clientOutOfDate) ...[ const SizedBox(width: 8), TextButton( - onPressed: openUpdateLink, + onPressed: () => unawaited(openUpdateLink()), style: TextButton.styleFrom( padding: const EdgeInsets.all(4), minimumSize: Size.zero, diff --git a/mobile/lib/widgets/common/dropdown_search_menu.dart b/mobile/lib/widgets/common/dropdown_search_menu.dart index bf0c75c8aa..54568c392a 100644 --- a/mobile/lib/widgets/common/dropdown_search_menu.dart +++ b/mobile/lib/widgets/common/dropdown_search_menu.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -114,7 +116,7 @@ class DropdownSearchMenu extends HookWidget { final bool highlight = AutocompleteHighlightedOption.of(context) == index; if (highlight) { SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { - Scrollable.ensureVisible(context, alignment: 0.5); + unawaited(Scrollable.ensureVisible(context, alignment: 0.5)); }, debugLabel: 'AutocompleteOptions.ensureVisible'); } return Container( diff --git a/mobile/lib/widgets/common/immich_loading_indicator.dart b/mobile/lib/widgets/common/immich_loading_indicator.dart index 52f957f7e7..1fdc5213df 100644 --- a/mobile/lib/widgets/common/immich_loading_indicator.dart +++ b/mobile/lib/widgets/common/immich_loading_indicator.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/widgets/common/immich_logo.dart'; @@ -9,11 +11,12 @@ class ImmichLoadingIndicator extends HookWidget { @override Widget build(BuildContext context) { - final logoAnimationController = useAnimationController(duration: const Duration(seconds: 6)) - ..reverse() - ..repeat(); + final logoAnimationController = useAnimationController(duration: const Duration(seconds: 6)); + unawaited(logoAnimationController.reverse()); + unawaited(logoAnimationController.repeat()); - final borderAnimationController = useAnimationController(duration: const Duration(seconds: 6))..repeat(); + final borderAnimationController = useAnimationController(duration: const Duration(seconds: 6)); + unawaited(borderAnimationController.repeat()); return Container( height: 80, diff --git a/mobile/lib/widgets/common/immich_sliver_app_bar.dart b/mobile/lib/widgets/common/immich_sliver_app_bar.dart index 22528b05d5..c5bce92cff 100644 --- a/mobile/lib/widgets/common/immich_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/immich_sliver_app_bar.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:math' as math; import 'package:auto_route/auto_route.dart'; @@ -304,13 +305,13 @@ class _SyncStatusIndicatorState extends ConsumerState<_SyncStatusIndicator> with // Control animations based on sync status if (isSyncing) { if (!_rotationController.isAnimating) { - _rotationController.repeat(); + unawaited(_rotationController.repeat()); } _dismissalController.reset(); } else { _rotationController.stop(); if (_dismissalController.status == AnimationStatus.dismissed) { - _dismissalController.forward(); + unawaited(_dismissalController.forward()); } } diff --git a/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart b/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart index 90dfd9c82a..0ceb98aa76 100644 --- a/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/mesmerizing_sliver_app_bar.dart @@ -134,7 +134,7 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S Future.delayed(const Duration(milliseconds: 100), () { if (mounted) { - _slideController.forward(); + unawaited(_slideController.forward()); } }); } @@ -228,7 +228,7 @@ class _ItemCountTextState extends ConsumerState<_ItemCountText> { @override void dispose() { - _reloadSubscription?.cancel(); + unawaited(_reloadSubscription?.cancel()); super.dispose(); } @@ -311,13 +311,17 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic void _startAnimationCycle() { if (_isZoomingIn) { - _zoomController.forward().then((_) { - _loadNextAsset(); - }); + unawaited( + _zoomController.forward().then((_) { + unawaited(_loadNextAsset()); + }), + ); } else { - _zoomController.reverse().then((_) { - _loadNextAsset(); - }); + unawaited( + _zoomController.reverse().then((_) { + unawaited(_loadNextAsset()); + }), + ); } } diff --git a/mobile/lib/widgets/common/person_sliver_app_bar.dart b/mobile/lib/widgets/common/person_sliver_app_bar.dart index 80dded2130..0e6829243c 100644 --- a/mobile/lib/widgets/common/person_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/person_sliver_app_bar.dart @@ -169,7 +169,7 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S Future.delayed(const Duration(milliseconds: 100), () { if (mounted) { - _slideController.forward(); + unawaited(_slideController.forward()); } }); } @@ -335,7 +335,7 @@ class _ItemCountTextState extends ConsumerState<_ItemCountText> { @override void dispose() { - _reloadSubscription?.cancel(); + unawaited(_reloadSubscription?.cancel()); super.dispose(); } @@ -416,13 +416,17 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic void _startAnimationCycle() { if (_isZoomingIn) { - _zoomController.forward().then((_) { - _loadNextAsset(); - }); + unawaited( + _zoomController.forward().then((_) { + unawaited(_loadNextAsset()); + }), + ); } else { - _zoomController.reverse().then((_) { - _loadNextAsset(); - }); + unawaited( + _zoomController.reverse().then((_) { + unawaited(_loadNextAsset()); + }), + ); } } diff --git a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart index 4d2dc5ef88..09a0367039 100644 --- a/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/remote_album_sliver_app_bar.dart @@ -172,7 +172,7 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> with S Future.delayed(const Duration(milliseconds: 100), () { if (mounted) { - _slideController.forward(); + unawaited(_slideController.forward()); } }); } @@ -309,7 +309,7 @@ class _ItemCountTextState extends ConsumerState<_ItemCountText> { @override void dispose() { - _reloadSubscription?.cancel(); + unawaited(_reloadSubscription?.cancel()); super.dispose(); } @@ -390,13 +390,17 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic void _startAnimationCycle() { if (_isZoomingIn) { - _zoomController.forward().then((_) { - _loadNextAsset(); - }); + unawaited( + _zoomController.forward().then((_) { + unawaited(_loadNextAsset()); + }), + ); } else { - _zoomController.reverse().then((_) { - _loadNextAsset(); - }); + unawaited( + _zoomController.reverse().then((_) { + unawaited(_loadNextAsset()); + }), + ); } } diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index 4c9b56646f..969c311bfb 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -70,7 +70,8 @@ class LoginForm extends HookConsumerWidget { final isOauthEnable = useState(false); final isPasswordLoginEnable = useState(false); final oAuthButtonLabel = useState('OAuth'); - final logoAnimationController = useAnimationController(duration: const Duration(seconds: 60))..repeat(); + final logoAnimationController = useAnimationController(duration: const Duration(seconds: 60)); + unawaited(logoAnimationController.repeat()); final serverInfo = ref.watch(serverInfoProvider); final warningMessage = useState(null); final loginFormKey = GlobalKey(); @@ -358,7 +359,7 @@ class LoginForm extends HookConsumerWidget { } SingleChildRenderObjectWidget buildVersionCompatWarning() { - checkVersionMismatch(); + unawaited(checkVersionMismatch()); if (warningMessage.value == null) { return const SizedBox.shrink(); diff --git a/mobile/lib/widgets/photo_view/src/controller/photo_view_controller.dart b/mobile/lib/widgets/photo_view/src/controller/photo_view_controller.dart index b9475a9ee2..08c300e9ae 100644 --- a/mobile/lib/widgets/photo_view/src/controller/photo_view_controller.dart +++ b/mobile/lib/widgets/photo_view/src/controller/photo_view_controller.dart @@ -209,7 +209,7 @@ class PhotoViewController implements PhotoViewControllerBase return; } _scaleAnimation = Tween(begin: from, end: to).animate(_scaleAnimationController); - _scaleAnimationController - ..value = 0.0 - ..fling(velocity: 0.4); + _scaleAnimationController.value = 0.0; + unawaited(_scaleAnimationController.fling(velocity: 0.4)); } void animatePosition(Offset from, Offset to) { @@ -250,9 +251,8 @@ class PhotoViewCoreState extends State return; } _positionAnimation = Tween(begin: from, end: to).animate(_positionAnimationController); - _positionAnimationController - ..value = 0.0 - ..fling(velocity: 0.4); + _positionAnimationController.value = 0.0; + unawaited(_positionAnimationController.fling(velocity: 0.4)); } void animateRotation(double from, double to) { @@ -260,9 +260,8 @@ class PhotoViewCoreState extends State return; } _rotationAnimation = Tween(begin: from, end: to).animate(_rotationAnimationController); - _rotationAnimationController - ..value = 0.0 - ..fling(velocity: 0.4); + _rotationAnimationController.value = 0.0; + unawaited(_rotationAnimationController.fling(velocity: 0.4)); } void onAnimationStatus(AnimationStatus status) { diff --git a/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart b/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart index db66cb962d..e55df57105 100644 --- a/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart +++ b/mobile/lib/widgets/photo_view/src/photo_view_wrappers.dart @@ -82,7 +82,6 @@ class _ImageWrapperState extends State { ImageStreamListener? _imageStreamListener; ImageStream? _imageStream; ImageChunkEvent? _loadingProgress; - ImageInfo? _imageInfo; bool _loading = true; Size? _imageSize; Object? _lastException; @@ -138,7 +137,6 @@ class _ImageWrapperState extends State { void setupCB() { _imageSize = Size(info.image.width.toDouble(), info.image.height.toDouble()); _loading = false; - _imageInfo = _imageInfo; _loadingProgress = null; _lastException = null; diff --git a/mobile/lib/widgets/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index 1c1d42639f..dbf54fd082 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; @@ -35,13 +36,16 @@ class AdvancedSettings extends HookConsumerWidget { final preferRemote = useState(ref.read(appConfigProvider).image.preferRemote); useValueChanged( preferRemote.value, - (_, __) => ref.read(settingsProvider).write(.imagePreferRemote, preferRemote.value), + (_, __) => unawaited(ref.read(settingsProvider).write(.imagePreferRemote, preferRemote.value)), ); final readonlyModeEnabled = useAppSettingsState(AppSettingsEnum.readonlyModeEnabled); final logLevel = Level.LEVELS[levelId.value].name; - useValueChanged(levelId.value, (_, __) => LogService.I.setLogLevel(Level.LEVELS[levelId.value].toLogLevel())); + useValueChanged( + levelId.value, + (_, __) => unawaited(LogService.I.setLogLevel(Level.LEVELS[levelId.value].toLogLevel())), + ); Future checkAndroidVersion() async { if (Platform.isAndroid) { @@ -54,12 +58,12 @@ class AdvancedSettings extends HookConsumerWidget { } useEffect(() { - () async { + unawaited(() async { isManageMediaSupported.value = await checkAndroidVersion(); if (isManageMediaSupported.value) { manageMediaAndroidPermission.value = await ref.read(permissionRepositoryProvider).hasManageMediaPermission(); } - }(); + }()); return null; }, []); diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart index f915df04f8..eda0d819d5 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_layout_settings.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -15,7 +17,7 @@ class LayoutSettings extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final tilesPerRow = useState(ref.read(appConfigProvider.select((s) => s.timeline.tilesPerRow))); useValueChanged(tilesPerRow.value, (_, __) { - ref.read(settingsProvider).write(.timelineTilesPerRow, tilesPerRow.value); + unawaited(ref.read(settingsProvider).write(.timelineTilesPerRow, tilesPerRow.value)); }); return Column( diff --git a/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart b/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart index 3ac72d6612..1f3f4bbcf0 100644 --- a/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart +++ b/mobile/lib/widgets/settings/asset_list_settings/asset_list_settings.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -21,7 +23,7 @@ class AssetListSettings extends HookConsumerWidget { valueNotifier: storageIndicator, title: 'theme_setting_asset_list_storage_indicator_title'.tr(), onChanged: (value) { - ref.read(settingsProvider).write(.timelineStorageIndicator, value); + unawaited(ref.read(settingsProvider).write(.timelineStorageIndicator, value)); ref.invalidate(appSettingsServiceProvider); ref.invalidate(settingsProvider); }, diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart index f65af6af9d..e3173dcdc7 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -14,7 +16,7 @@ class ImageViewerQualitySetting extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final isOriginal = useState(ref.read(appConfigProvider).image.loadOriginal); useValueChanged(isOriginal.value, (_, __) { - ref.read(settingsProvider).write(.imageLoadOriginal, isOriginal.value); + unawaited(ref.read(settingsProvider).write(.imageLoadOriginal, isOriginal.value)); }); return Column( diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart index 730521e3c1..c785e8fed4 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -13,7 +15,7 @@ class ImageViewerTapToNavigateSetting extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final tapToNavigate = useState(ref.read(appConfigProvider).viewer.tapToNavigate); useValueChanged(tapToNavigate.value, (_, __) { - ref.read(settingsProvider).write(.viewerTapToNavigate, tapToNavigate.value); + unawaited(ref.read(settingsProvider).write(.viewerTapToNavigate, tapToNavigate.value)); }); return Column( diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/slideshow_settings.dart b/mobile/lib/widgets/settings/asset_viewer_settings/slideshow_settings.dart index af361943ec..ec52d23ca8 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/slideshow_settings.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/slideshow_settings.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -22,16 +24,16 @@ class SlideshowSettings extends HookConsumerWidget { final useDirection = useState(slideshow.direction); useValueChanged(useRepeat.value, (_, __) { - ref.read(settingsProvider).write(.slideshowRepeat, useRepeat.value); + unawaited(ref.read(settingsProvider).write(.slideshowRepeat, useRepeat.value)); }); useValueChanged(useDuration.value, (_, __) { - ref.read(settingsProvider).write(.slideshowDuration, useDuration.value); + unawaited(ref.read(settingsProvider).write(.slideshowDuration, useDuration.value)); }); useValueChanged(useLook.value, (_, __) { - ref.read(settingsProvider).write(.slideshowLook, useLook.value); + unawaited(ref.read(settingsProvider).write(.slideshowLook, useLook.value)); }); useValueChanged(useDirection.value, (_, __) { - ref.read(settingsProvider).write(.slideshowDirection, useDirection.value); + unawaited(ref.read(settingsProvider).write(.slideshowDirection, useDirection.value)); }); return Column( diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart b/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart index 81929d95b9..2b302e0427 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -17,13 +19,13 @@ class VideoViewerSettings extends HookConsumerWidget { final useOriginalVideo = useState(viewer.loadOriginalVideo); useValueChanged(useAutoPlayVideo.value, (_, __) { - ref.read(settingsProvider).write(.viewerAutoPlayVideo, useAutoPlayVideo.value); + unawaited(ref.read(settingsProvider).write(.viewerAutoPlayVideo, useAutoPlayVideo.value)); }); useValueChanged(useLoopVideo.value, (_, __) { - ref.read(settingsProvider).write(.viewerLoopVideo, useLoopVideo.value); + unawaited(ref.read(settingsProvider).write(.viewerLoopVideo, useLoopVideo.value)); }); useValueChanged(useOriginalVideo.value, (_, __) { - ref.read(settingsProvider).write(.viewerLoadOriginalVideo, useOriginalVideo.value); + unawaited(ref.read(settingsProvider).write(.viewerLoadOriginalVideo, useOriginalVideo.value)); }); return Column( diff --git a/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart b/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart index 44a1a1d7a9..f0f6435f90 100644 --- a/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart +++ b/mobile/lib/widgets/settings/backup_settings/drift_backup_settings.dart @@ -232,7 +232,7 @@ class _BackupOnlyWhenChargingButton extends ConsumerWidget { titleKey: "charging", subtitleKey: "charging_requirement_mobile_backup", onChanged: (value) { - fgService.configure(requireCharging: value); + unawaited(fgService.configure(requireCharging: value)); }, ); } diff --git a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart index 7bd604ae5e..1871dc8a62 100644 --- a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart +++ b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart @@ -132,7 +132,7 @@ class SyncStatusAndActions extends HookConsumerWidget { leading: const Icon(Icons.sync), trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).localSyncStatus), onTap: () { - ref.read(backgroundSyncProvider).syncLocal(full: true); + unawaited(ref.read(backgroundSyncProvider).syncLocal(full: true)); }, ), SettingListTile( @@ -141,7 +141,7 @@ class SyncStatusAndActions extends HookConsumerWidget { leading: const Icon(Icons.cloud_sync), trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).remoteSyncStatus), onTap: () { - ref.read(backgroundSyncProvider).syncRemote(); + unawaited(ref.read(backgroundSyncProvider).syncRemote()); }, ), if (CurrentPlatform.isIOS && serverVersion.isAtLeast(major: 2, minor: 5)) @@ -158,7 +158,7 @@ class SyncStatusAndActions extends HookConsumerWidget { subtitle: "tap_to_run_job".t(context: context), trailing: _SyncStatusIcon(status: ref.watch(syncStatusProvider).hashJobStatus), onTap: () { - ref.read(backgroundSyncProvider).hashAssets(); + unawaited(ref.read(backgroundSyncProvider).hashAssets()); }, ), const Divider(height: 1), diff --git a/mobile/lib/widgets/settings/free_up_space_settings.dart b/mobile/lib/widgets/settings/free_up_space_settings.dart index dbec3a2dcb..72e85ac821 100644 --- a/mobile/lib/widgets/settings/free_up_space_settings.dart +++ b/mobile/lib/widgets/settings/free_up_space_settings.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -30,9 +32,9 @@ class _FreeUpSpaceSettingsState extends ConsumerState { @override void initState() { super.initState(); - WakelockPlus.enable(); + unawaited(WakelockPlus.enable()); WidgetsBinding.instance.addPostFrameCallback((_) { - _initializeAlbumDefaults(); + unawaited(_initializeAlbumDefaults()); }); } @@ -68,7 +70,7 @@ class _FreeUpSpaceSettingsState extends ConsumerState { void _goToScanStep() { ref.read(hapticFeedbackProvider.notifier).mediumImpact(); setState(() => _currentStep = CleanupStep.scan); - _scanAssets(); + unawaited(_scanAssets()); } void _setPresetDate(int daysAgo) { @@ -169,13 +171,13 @@ class _FreeUpSpaceSettingsState extends ConsumerState { void _showAssetsPreview(List assets) { ref.read(hapticFeedbackProvider.notifier).mediumImpact(); - context.pushRoute(CleanupPreviewRoute(assets: assets)); + unawaited(context.pushRoute(CleanupPreviewRoute(assets: assets))); } @override void dispose() { super.dispose(); - WakelockPlus.disable(); + unawaited(WakelockPlus.disable()); } @override diff --git a/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart b/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart index e8310caed4..162f1252a8 100644 --- a/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart +++ b/mobile/lib/widgets/settings/networking_settings/endpoint_input.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -53,7 +55,7 @@ class EndpointInputState extends ConsumerState { void _onOutFocus() { if (!focusNode.hasFocus && isInputValid) { - validateAuxilaryServerUrl(); + unawaited(validateAuxilaryServerUrl()); } } diff --git a/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart index f3c2b6c97f..68278e0e1d 100644 --- a/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart +++ b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -44,13 +46,13 @@ class ExternalNetworkPreference extends HookConsumerWidget { entries.value.insert(newIndex, entry); entries.value = [...entries.value]; - saveEndpointList(); + unawaited(saveEndpointList()); } void handleDismiss(int index) { entries.value = [...entries.value..removeAt(index)]; - saveEndpointList(); + unawaited(saveEndpointList()); } Widget proxyDecorator(Widget child, int _, Animation animation) { diff --git a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart index 7e6e169de7..e7510053e3 100644 --- a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart +++ b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -21,7 +23,7 @@ class NetworkingSettings extends HookConsumerWidget { final currentEndpoint = getServerUrl(); final featureEnabled = useState(ref.read(appConfigProvider).network.autoEndpointSwitching); useValueChanged(featureEnabled.value, (_, __) { - ref.read(settingsProvider).write(.networkAutoEndpointSwitching, featureEnabled.value); + unawaited(ref.read(settingsProvider).write(.networkAutoEndpointSwitching, featureEnabled.value)); }); Future checkWifiReadPermission() async { @@ -83,7 +85,7 @@ class NetworkingSettings extends HookConsumerWidget { useEffect(() { if (featureEnabled.value == true) { - checkWifiReadPermission(); + unawaited(checkWifiReadPermission()); } return null; }, [featureEnabled.value]); diff --git a/mobile/lib/widgets/settings/notification_setting.dart b/mobile/lib/widgets/settings/notification_setting.dart index ee2e15f52b..8c858a231b 100644 --- a/mobile/lib/widgets/settings/notification_setting.dart +++ b/mobile/lib/widgets/settings/notification_setting.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -17,20 +19,22 @@ class NotificationSetting extends HookConsumerWidget { void openAppNotificationSettings(BuildContext ctx) { ctx.pop(); - openAppSettings(); + unawaited(openAppSettings()); } // When permissions are permanently denied, you need to go to settings to // allow them void showPermissionsDialog() { - showDialog( - context: context, - builder: (ctx) => AlertDialog( - content: const Text('notification_permission_dialog_content').tr(), - actions: [ - TextButton(child: const Text('cancel').tr(), onPressed: () => ctx.pop()), - TextButton(onPressed: () => openAppNotificationSettings(ctx), child: const Text('settings').tr()), - ], + unawaited( + showDialog( + context: context, + builder: (ctx) => AlertDialog( + content: const Text('notification_permission_dialog_content').tr(), + actions: [ + TextButton(child: const Text('cancel').tr(), onPressed: () => ctx.pop()), + TextButton(onPressed: () => openAppNotificationSettings(ctx), child: const Text('settings').tr()), + ], + ), ), ); } diff --git a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart index 1defd2df44..b0624dca23 100644 --- a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -26,16 +28,16 @@ class PrimaryColorSetting extends HookConsumerWidget { } void onUseSystemColorChange(bool newValue) { - ref.read(settingsProvider).write(.themeDynamic, newValue); + unawaited(ref.read(settingsProvider).write(.themeDynamic, newValue)); popBottomSheet(); } void onPrimaryColorChange(ImmichColorPreset colorPreset) { - ref.read(settingsProvider).write(.themePrimaryColor, colorPreset); + unawaited(ref.read(settingsProvider).write(.themePrimaryColor, colorPreset)); //turn off system color setting if (themeConfig.dynamicTheme) { - ref.read(settingsProvider).write(.themeDynamic, false); + unawaited(ref.read(settingsProvider).write(.themeDynamic, false)); } popBottomSheet(); } diff --git a/mobile/lib/widgets/settings/preference_settings/share_setting.dart b/mobile/lib/widgets/settings/preference_settings/share_setting.dart index 2435810566..1881703023 100644 --- a/mobile/lib/widgets/settings/preference_settings/share_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/share_setting.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -19,7 +21,7 @@ class ShareSetting extends HookConsumerWidget { void onChanged(ShareAssetType? value) { if (value != null) { fileType.value = value; - ref.read(settingsProvider).write(SettingsKey.shareFileType, value); + unawaited(ref.read(settingsProvider).write(SettingsKey.shareFileType, value)); } } diff --git a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart index ffeeceae02..ec84d7be01 100644 --- a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -22,7 +24,7 @@ class ThemeSetting extends HookConsumerWidget { void onThemeChange(bool isDark) { currentTheme.value = isDark ? ThemeMode.dark : ThemeMode.light; - ref.read(settingsProvider).write(.themeMode, currentTheme.value); + unawaited(ref.read(settingsProvider).write(.themeMode, currentTheme.value)); } void onSystemThemeChange(bool isSystem) { @@ -39,11 +41,11 @@ class ThemeSetting extends HookConsumerWidget { currentTheme.value = ThemeMode.dark; } } - ref.read(settingsProvider).write(.themeMode, currentTheme.value); + unawaited(ref.read(settingsProvider).write(.themeMode, currentTheme.value)); } void onSurfaceColorSettingChange(bool useColorfulInterface) { - ref.read(settingsProvider).write(.themeColorfulInterface, useColorfulInterface); + unawaited(ref.read(settingsProvider).write(.themeColorfulInterface, useColorfulInterface)); colorfulInterface.value = useColorfulInterface; } diff --git a/mobile/lib/wm_executor.dart b/mobile/lib/wm_executor.dart index e873c5f76d..aac6d3e174 100644 --- a/mobile/lib/wm_executor.dart +++ b/mobile/lib/wm_executor.dart @@ -145,7 +145,7 @@ class _Executor extends Mixinable<_Executor> with _ExecutorLogger { void _schedule() { final availableWorker = _pool.firstWhereOrNull((worker) => worker.taskId == null && worker.initialized); if (availableWorker == null) { - _ensureWorkersInitialized(); + unawaited(_ensureWorkersInitialized()); return; } if (_queue.isEmpty) { @@ -153,26 +153,28 @@ class _Executor extends Mixinable<_Executor> with _ExecutorLogger { } final task = _queue.removeFirst(); - availableWorker - .work(task) - .then( - (value) { - //might be completed by cancel and it is normal. - //Assuming that worker finished with error and cleaned gracefully - task.complete(value, null, null); - }, - onError: (error, st) { - task.complete(null, error, st); - }, - ) - .whenComplete(() { - if (_dynamicSpawning && _queue.isEmpty) { - // Retire the idle worker; shutdown() nulls its fields so the husk - // stays pooled and is revived by initialize() if work arrives. - unawaited(availableWorker.shutdown()); - } - _schedule(); - }); + unawaited( + availableWorker + .work(task) + .then( + (value) { + //might be completed by cancel and it is normal. + //Assuming that worker finished with error and cleaned gracefully + task.complete(value, null, null); + }, + onError: (error, st) { + task.complete(null, error, st); + }, + ) + .whenComplete(() { + if (_dynamicSpawning && _queue.isEmpty) { + // Retire the idle worker; shutdown() nulls its fields so the husk + // stays pooled and is revived by initialize() if work arrives. + unawaited(availableWorker.shutdown()); + } + _schedule(); + }), + ); } @override diff --git a/mobile/packages/ui/test/formatted_text_test.dart b/mobile/packages/ui/test/formatted_text_test.dart index c3901cd802..5f26822855 100644 --- a/mobile/packages/ui/test/formatted_text_test.dart +++ b/mobile/packages/ui/test/formatted_text_test.dart @@ -59,7 +59,7 @@ void main() { ); final text = tester.widget(find.byType(Text)); - final richText = text.textSpan as TextSpan; + final richText = text.textSpan! as TextSpan; expect(richText.style?.fontSize, 16); expect(richText.style?.color, Colors.purple); diff --git a/mobile/test/presentation/widgets/timeline/timeline_args_test.dart b/mobile/test/presentation/widgets/timeline/timeline_args_test.dart index 0828e8e989..5c03998bfc 100644 --- a/mobile/test/presentation/widgets/timeline/timeline_args_test.dart +++ b/mobile/test/presentation/widgets/timeline/timeline_args_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: close_sinks + import 'dart:async'; import 'package:flutter/material.dart'; diff --git a/mobile/test/services/deep_link_service_test.dart b/mobile/test/services/deep_link_service_test.dart index ff090367ea..16fa392f27 100644 --- a/mobile/test/services/deep_link_service_test.dart +++ b/mobile/test/services/deep_link_service_test.dart @@ -115,7 +115,7 @@ void main() { final route = await sut.handleMyImmichApp(link('/albums/$_albumId/photos/$_assetId'), ref); expect(route, isA()); - expect((route!.args as AssetViewerRouteArgs).currentAlbum, _album); + expect((route!.args! as AssetViewerRouteArgs).currentAlbum, _album); }); test('still opens the viewer when the album cannot be resolved', () async { @@ -125,7 +125,7 @@ void main() { final route = await sut.handleMyImmichApp(link('/albums/$_albumId/photos/$_assetId'), ref); expect(route, isA()); - expect((route!.args as AssetViewerRouteArgs).currentAlbum, isNull); + expect((route!.args! as AssetViewerRouteArgs).currentAlbum, isNull); }); test('plain photo link has no album', () async { @@ -134,7 +134,7 @@ void main() { final route = await sut.handleMyImmichApp(link('/photos/$_assetId'), ref); expect(route, isA()); - expect((route!.args as AssetViewerRouteArgs).currentAlbum, isNull); + expect((route!.args! as AssetViewerRouteArgs).currentAlbum, isNull); verifyNever(() => remoteAlbumService.get(any())); }); } From 6b6058c4631a4c8ec551a960739210c379577695 Mon Sep 17 00:00:00 2001 From: Giacomo Pinato Date: Thu, 30 Jul 2026 21:41:49 +0200 Subject: [PATCH 17/81] feat: store null instead of empty string for album.description (#30123) Addresses the first column of issue #28832: album.description now stores and returns null instead of an empty string. Co-authored-by: Giacomo Pinato --- .../drift_album_api_repository.dart | 8 ++- open-api/immich-openapi-specs.json | 41 +++++++++++++-- packages/sdk/src/fetch-client.ts | 4 +- server/src/dtos/album.dto.ts | 52 +++++++++++++++++-- .../1784664555996-AlbumDescriptionNullable.ts | 13 +++++ server/src/schema/tables/album.table.ts | 4 +- server/src/services/sync.service.ts | 10 +++- web/src/lib/modals/AlbumEditModal.svelte | 2 +- .../[[assetId=id]]/AlbumDescription.svelte | 2 +- 9 files changed, 118 insertions(+), 18 deletions(-) create mode 100644 server/src/schema/migrations/1784664555996-AlbumDescriptionNullable.ts diff --git a/mobile/lib/repositories/drift_album_api_repository.dart b/mobile/lib/repositories/drift_album_api_repository.dart index e0d4cc4632..8c21af6599 100644 --- a/mobile/lib/repositories/drift_album_api_repository.dart +++ b/mobile/lib/repositories/drift_album_api_repository.dart @@ -25,7 +25,9 @@ class DriftAlbumApiRepository extends ApiRepository { _api.createAlbum( CreateAlbumDto( albumName: name, - description: description == null ? const Optional.absent() : Optional.present(description), + description: description == null + ? const Optional.absent() + : Optional.present(description.isEmpty ? null : description), assetIds: Optional.present(assetIds.toList()), ), ), @@ -88,7 +90,9 @@ class DriftAlbumApiRepository extends ApiRepository { albumId, UpdateAlbumDto( albumName: name == null ? const Optional.absent() : Optional.present(name), - description: description == null ? const Optional.absent() : Optional.present(description), + description: description == null + ? const Optional.absent() + : Optional.present(description.isEmpty ? null : description), albumThumbnailAssetId: thumbnailAssetId == null ? const Optional.absent() : Optional.present(thumbnailAssetId), diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index dd5d4dce45..bc3bf82094 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -16585,7 +16585,18 @@ }, "description": { "description": "Album description", - "type": "string" + "type": "string", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v3", + "state": "Updated", + "description": "An empty string is returned instead of null for backwards compatibility; null will be returned in v4." + } + ] }, "endDate": { "description": "End date (latest asset)", @@ -18443,7 +18454,19 @@ }, "description": { "description": "Album description", - "type": "string" + "nullable": true, + "type": "string", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v3", + "state": "Updated", + "description": "Sending an empty string is deprecated; send null instead. Empty strings will no longer be coerced to null in v4." + } + ] } }, "required": [ @@ -27115,7 +27138,19 @@ }, "description": { "description": "Album description", - "type": "string" + "nullable": true, + "type": "string", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v3", + "state": "Updated", + "description": "Sending an empty string is deprecated; send null instead. Empty strings will no longer be coerced to null in v4." + } + ] }, "isActivityEnabled": { "description": "Enable activity feed", diff --git a/packages/sdk/src/fetch-client.ts b/packages/sdk/src/fetch-client.ts index a7e0d9511e..3ac958ce2d 100644 --- a/packages/sdk/src/fetch-client.ts +++ b/packages/sdk/src/fetch-client.ts @@ -540,7 +540,7 @@ export type CreateAlbumDto = { /** Initial asset IDs */ assetIds?: string[]; /** Album description */ - description?: string; + description?: string | null; }; export type AlbumsAddAssetsDto = { /** Album IDs */ @@ -567,7 +567,7 @@ export type UpdateAlbumDto = { /** Album thumbnail asset ID */ albumThumbnailAssetId?: string; /** Album description */ - description?: string; + description?: string | null; /** Enable activity feed */ isActivityEnabled?: boolean; order?: AssetOrder; diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index 3c871f672d..1e3b3b6193 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -34,7 +34,22 @@ const AlbumUserCreateSchema = z const CreateAlbumSchema = z .object({ albumName: z.string().describe('Album name'), - description: z.string().optional().describe('Album description'), + // TODO: drop the empty-string-to-null transform in v4 (clients should send null) + description: z + .string() + .nullable() + .transform((value) => (value === '' ? null : value)) + .optional() + .describe('Album description') + .meta({ + ...new HistoryBuilder() + .added('v1') + .updated( + 'v3', + 'Sending an empty string is deprecated; send null instead. Empty strings will no longer be coerced to null in v4.', + ) + .getExtensions(), + }), albumUsers: z.array(AlbumUserCreateSchema).optional().describe('Album users'), assetIds: z.array(z.uuidv4()).optional().describe('Initial asset IDs'), }) @@ -57,7 +72,22 @@ const AlbumsAddAssetsResponseSchema = z const UpdateAlbumSchema = z .object({ albumName: z.string().optional().describe('Album name'), - description: z.string().optional().describe('Album description'), + // TODO: drop the empty-string-to-null transform in v4 (clients should send null) + description: z + .string() + .nullable() + .transform((value) => (value === '' ? null : value)) + .optional() + .describe('Album description') + .meta({ + ...new HistoryBuilder() + .added('v1') + .updated( + 'v3', + 'Sending an empty string is deprecated; send null instead. Empty strings will no longer be coerced to null in v4.', + ) + .getExtensions(), + }), albumThumbnailAssetId: z.uuidv4().optional().describe('Album thumbnail asset ID'), isActivityEnabled: z.boolean().optional().describe('Enable activity feed'), order: AssetOrderSchema.optional(), @@ -110,7 +140,18 @@ export const AlbumResponseSchema = z .object({ id: z.uuidv4().describe('Album ID'), albumName: z.string().describe('Album name'), - description: z.string().describe('Album description'), + description: z + .string() + .describe('Album description') + .meta({ + ...new HistoryBuilder() + .added('v1') + .updated( + 'v3', + 'An empty string is returned instead of null for backwards compatibility; null will be returned in v4.', + ) + .getExtensions(), + }), // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. createdAt: z.string().meta({ format: 'date-time' }).describe('Creation date'), // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. @@ -171,7 +212,7 @@ export type MapAlbumDto = { assets?: ShallowDehydrateObject[]; sharedLinks?: ShallowDehydrateObject[]; albumName: string; - description: string; + description: string | null; albumThumbnailAssetId: string | null; createdAt: Date; updatedAt: Date; @@ -207,7 +248,8 @@ export const mapAlbum = (entity: MaybeDehydrated): AlbumResponseDto return { albumName: entity.albumName, - description: entity.description, + // TODO: return null instead of '' in v4 + description: entity.description ?? '', albumThumbnailAssetId: entity.albumThumbnailAssetId, createdAt: asDateTimeString(entity.createdAt), updatedAt: asDateTimeString(entity.updatedAt), diff --git a/server/src/schema/migrations/1784664555996-AlbumDescriptionNullable.ts b/server/src/schema/migrations/1784664555996-AlbumDescriptionNullable.ts new file mode 100644 index 0000000000..3be799f498 --- /dev/null +++ b/server/src/schema/migrations/1784664555996-AlbumDescriptionNullable.ts @@ -0,0 +1,13 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "album" ALTER COLUMN "description" DROP NOT NULL;`.execute(db); + await sql`ALTER TABLE "album" ALTER COLUMN "description" SET DEFAULT NULL;`.execute(db); + await sql`UPDATE "album" SET "description" = NULL WHERE "description" = '';`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`UPDATE "album" SET "description" = '' WHERE "description" IS NULL;`.execute(db); + await sql`ALTER TABLE "album" ALTER COLUMN "description" SET DEFAULT ''::text;`.execute(db); + await sql`ALTER TABLE "album" ALTER COLUMN "description" SET NOT NULL;`.execute(db); +} diff --git a/server/src/schema/tables/album.table.ts b/server/src/schema/tables/album.table.ts index f54658be65..c0d13d4902 100644 --- a/server/src/schema/tables/album.table.ts +++ b/server/src/schema/tables/album.table.ts @@ -36,8 +36,8 @@ export class AlbumTable { @UpdateDateColumn() updatedAt!: Generated; - @Column({ type: 'text', default: '' }) - description!: Generated; + @Column({ type: 'text', nullable: true }) + description!: string | null; @DeleteDateColumn() deletedAt!: Timestamp | null; diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index 87fc40306f..e3842b1503 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -441,7 +441,12 @@ export class SyncService extends BaseService { const upserts = this.syncRepository.album.getUpserts({ ...options, ack: checkpointMap[upsertType] }); for await (const { updateId, ...data } of upserts) { const albumUsers = await this.syncRepository.album.getAlbumUsers(data.id); - send(response, { type: upsertType, ids: [updateId], data: syncAlbumV2ToV1(data, albumUsers) }); + send(response, { + type: upsertType, + ids: [updateId], + // TODO: return null instead of '' in v4 + data: syncAlbumV2ToV1({ ...data, description: data.description ?? '' }, albumUsers), + }); } } @@ -455,7 +460,8 @@ export class SyncService extends BaseService { const upsertType = SyncEntityType.AlbumV2; const upserts = this.syncRepository.album.getUpserts({ ...options, ack: checkpointMap[upsertType] }); for await (const { updateId, ...data } of upserts) { - send(response, { type: upsertType, ids: [updateId], data }); + // TODO: return null instead of '' in v4 + send(response, { type: upsertType, ids: [updateId], data: { ...data, description: data.description ?? '' } }); } } diff --git a/web/src/lib/modals/AlbumEditModal.svelte b/web/src/lib/modals/AlbumEditModal.svelte index 7c4ee9b626..c5386a6a0d 100644 --- a/web/src/lib/modals/AlbumEditModal.svelte +++ b/web/src/lib/modals/AlbumEditModal.svelte @@ -17,7 +17,7 @@ let description = $state(album.description); const onSubmit = async () => { - const success = await handleUpdateAlbum(album, { albumName, description }); + const success = await handleUpdateAlbum(album, { albumName, description: description || null }); if (success) { onClose(); } diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/AlbumDescription.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/AlbumDescription.svelte index 1d80fcb766..cb2fb2a9bb 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/AlbumDescription.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/AlbumDescription.svelte @@ -20,7 +20,7 @@ const response = await updateAlbumInfo({ id, updateAlbumDto: { - description, + description: description || null, }, }); eventManager.emit('AlbumUpdate', response); From aa565f5ca0773c6e76b100d9b2fa99924a6dec45 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:47:34 -0400 Subject: [PATCH 18/81] chore(deps): update github-actions (major) (#30309) chore(deps): update github-actions Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/pr-labeler.yml | 2 +- .github/workflows/prepare-release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index c2664839e3..d924c84596 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -19,6 +19,6 @@ jobs: permission-contents: read permission-pull-requests: write - - uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0 + - uses: actions/labeler@bf12e9b00b37c5c0ca2b87b79b2daf7891dbda13 # v7.0.0 with: repo-token: ${{ steps.token.outputs.token }} diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 05c6f82977..5150896192 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -74,7 +74,7 @@ jobs: # TODO move to mise - name: Install uv - uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 - name: Bump version env: From 2fa07c0a4283a2158f55b146c6e64d25d497611f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:49:32 -0400 Subject: [PATCH 19/81] chore(deps): update grafana/grafana docker tag to v12.4.6-ubuntu (#30304) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker/docker-compose.prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index aecb6b1dad..013ff266fa 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -97,7 +97,7 @@ services: command: ['./run.sh', '-disable-reporting'] ports: - 3000:3000 - image: grafana/grafana:12.4.5-ubuntu@sha256:00396460e499415c828b7c298f19287c8a0f95e72412ee37ac11723655c2d6b9 + image: grafana/grafana:12.4.6-ubuntu@sha256:35c75489ff2e2e69c53977a180de491643d51fdb32108f9335317527978daed7 volumes: - grafana-data:/var/lib/grafana From 0f4dddaf84636731212ed65ae41a2762d617484b Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 30 Jul 2026 15:55:21 -0400 Subject: [PATCH 20/81] fix: migration order (#30424) --- ...ptionNullable.ts => 1784986754474-AlbumDescriptionNullable.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename server/src/schema/migrations/{1784664555996-AlbumDescriptionNullable.ts => 1784986754474-AlbumDescriptionNullable.ts} (100%) diff --git a/server/src/schema/migrations/1784664555996-AlbumDescriptionNullable.ts b/server/src/schema/migrations/1784986754474-AlbumDescriptionNullable.ts similarity index 100% rename from server/src/schema/migrations/1784664555996-AlbumDescriptionNullable.ts rename to server/src/schema/migrations/1784986754474-AlbumDescriptionNullable.ts From 56ce7176b1d0e573a09134a9ea019dc096034067 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:56:24 -0400 Subject: [PATCH 21/81] chore(deps): update dependency opentofu to v1.12.5 (#30299) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- deployment/mise.lock | 44 ++++++++++++++++++++++---------------------- deployment/mise.toml | 2 +- mise.lock | 44 ++++++++++++++++++++++---------------------- mise.toml | 2 +- 4 files changed, 46 insertions(+), 46 deletions(-) diff --git a/deployment/mise.lock b/deployment/mise.lock index 663ae4985d..018620a4a5 100644 --- a/deployment/mise.lock +++ b/deployment/mise.lock @@ -1,43 +1,43 @@ # @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html [[tools.opentofu]] -version = "1.11.6" +version = "1.12.5" backend = "aqua:opentofu/opentofu" [tools.opentofu."platforms.linux-arm64"] -checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850" -url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536382" +checksum = "sha256:e67e9da2b1ddf5050ebee62a584cb826eafe1dfd3827d7ec20899ac62791ed1a" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602545" [tools.opentofu."platforms.linux-arm64-musl"] -checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850" -url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536382" +checksum = "sha256:e67e9da2b1ddf5050ebee62a584cb826eafe1dfd3827d7ec20899ac62791ed1a" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602545" [tools.opentofu."platforms.linux-x64"] -checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e" -url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536401" +checksum = "sha256:a6894d45ae7a17ce83189cce8fe04b5a65f68cefceb62455b5a6a89fa53ab38f" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602624" [tools.opentofu."platforms.linux-x64-musl"] -checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e" -url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536401" +checksum = "sha256:a6894d45ae7a17ce83189cce8fe04b5a65f68cefceb62455b5a6a89fa53ab38f" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602624" [tools.opentofu."platforms.macos-arm64"] -checksum = "sha256:62d7fa8539e13b444827aa0a3b90c5972da5c47e8f8882d9dcf2e430e78840c1" -url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_arm64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536399" +checksum = "sha256:2ae38150a667f5c0bd57b318d18ad8091d08f93fcca40345f3d88998661de5a9" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_darwin_arm64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602544" [tools.opentofu."platforms.macos-x64"] -checksum = "sha256:1408cdef1c380f914565e6b4bb70794c6b163f195fcb233357f3d6c5745906b6" -url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_amd64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536384" +checksum = "sha256:1012d8f3d4567bcbcd1f2c7d766feca39a30bced32fb8be47e1887fbbee2456d" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_darwin_amd64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602621" [tools.opentofu."platforms.windows-x64"] -checksum = "sha256:27323f70c875b8251bfd7e61a4cffc3ebff4e56ed1e611b955016f0c7077367e" -url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_windows_amd64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/391536406" +checksum = "sha256:af11850b496f3720e0184084c56d8b43aa74ea92d2338978bf368d70c96473f1" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_windows_amd64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602547" [[tools.terragrunt]] version = "1.0.3" diff --git a/deployment/mise.toml b/deployment/mise.toml index 2e26da09f6..7098b79c99 100644 --- a/deployment/mise.toml +++ b/deployment/mise.toml @@ -1,6 +1,6 @@ [tools] terragrunt = "1.1.1" -opentofu = "1.12.4" +opentofu = "1.12.5" [tasks."tg:fmt"] run = "terragrunt hclfmt" diff --git a/mise.lock b/mise.lock index 656d11b3eb..3b05f22889 100644 --- a/mise.lock +++ b/mise.lock @@ -252,43 +252,43 @@ version = "7.5.0" backend = "npm:oazapfts" [[tools.opentofu]] -version = "1.12.4" +version = "1.12.5" backend = "aqua:opentofu/opentofu" [tools.opentofu."platforms.linux-arm64"] -checksum = "sha256:dc7bfcd93ce9795a86c58fbf71efd013c39dcd1febb13c9cd3555c43b9c2403a" -url = "https://github.com/opentofu/opentofu/releases/download/v1.12.4/tofu_1.12.4_linux_arm64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/475646678" +checksum = "sha256:e67e9da2b1ddf5050ebee62a584cb826eafe1dfd3827d7ec20899ac62791ed1a" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602545" [tools.opentofu."platforms.linux-arm64-musl"] -checksum = "sha256:dc7bfcd93ce9795a86c58fbf71efd013c39dcd1febb13c9cd3555c43b9c2403a" -url = "https://github.com/opentofu/opentofu/releases/download/v1.12.4/tofu_1.12.4_linux_arm64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/475646678" +checksum = "sha256:e67e9da2b1ddf5050ebee62a584cb826eafe1dfd3827d7ec20899ac62791ed1a" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_linux_arm64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602545" [tools.opentofu."platforms.linux-x64"] -checksum = "sha256:81836d0f12b4fe9013b85586349f993def9429b6383bb77cdd6c2f3a9d9aac24" -url = "https://github.com/opentofu/opentofu/releases/download/v1.12.4/tofu_1.12.4_linux_amd64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/475646677" +checksum = "sha256:a6894d45ae7a17ce83189cce8fe04b5a65f68cefceb62455b5a6a89fa53ab38f" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602624" [tools.opentofu."platforms.linux-x64-musl"] -checksum = "sha256:81836d0f12b4fe9013b85586349f993def9429b6383bb77cdd6c2f3a9d9aac24" -url = "https://github.com/opentofu/opentofu/releases/download/v1.12.4/tofu_1.12.4_linux_amd64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/475646677" +checksum = "sha256:a6894d45ae7a17ce83189cce8fe04b5a65f68cefceb62455b5a6a89fa53ab38f" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_linux_amd64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602624" [tools.opentofu."platforms.macos-arm64"] -checksum = "sha256:7c06e4390d9ccd467773e37ff1c3d833c7ca0c24742cd9e9ad47284bea472247" -url = "https://github.com/opentofu/opentofu/releases/download/v1.12.4/tofu_1.12.4_darwin_arm64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/475646544" +checksum = "sha256:2ae38150a667f5c0bd57b318d18ad8091d08f93fcca40345f3d88998661de5a9" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_darwin_arm64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602544" [tools.opentofu."platforms.macos-x64"] -checksum = "sha256:ead1d2ce643addb4ffeb93240b9377ae1c2fd793a6bd22d65922ac37adfdf546" -url = "https://github.com/opentofu/opentofu/releases/download/v1.12.4/tofu_1.12.4_darwin_amd64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/475646688" +checksum = "sha256:1012d8f3d4567bcbcd1f2c7d766feca39a30bced32fb8be47e1887fbbee2456d" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_darwin_amd64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602621" [tools.opentofu."platforms.windows-x64"] -checksum = "sha256:a4d86a07755c8d151f20f945e6cfb5b40deeed942af36a9bd385c5c2e965d5dd" -url = "https://github.com/opentofu/opentofu/releases/download/v1.12.4/tofu_1.12.4_windows_amd64.tar.gz" -url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/475646543" +checksum = "sha256:af11850b496f3720e0184084c56d8b43aa74ea92d2338978bf368d70c96473f1" +url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12.5_windows_amd64.tar.gz" +url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602547" [[tools.pnpm]] version = "11.13.1" diff --git a/mise.toml b/mise.toml index 5ff9db5819..d069dc1b99 100644 --- a/mise.toml +++ b/mise.toml @@ -18,7 +18,7 @@ config_roots = [ node = "24.15.0" pnpm = "11.13.1" terragrunt = "1.1.1" -opentofu = "1.12.4" +opentofu = "1.12.5" "npm:@openapitools/openapi-generator-cli" = "2.40.1" "npm:oazapfts" = "7.5.0" "github:extism/cli" = "1.6.3" From 405020eeeed88f803543eabbd2547350e172be3f Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Fri, 31 Jul 2026 01:40:36 +0530 Subject: [PATCH 22/81] chore: enable use_build_context_synchronously lint (#30367) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- mobile/analysis_options.yaml | 2 +- mobile/lib/main.dart | 29 ++++--- .../drift_backup_album_selection.page.dart | 4 + .../drift_backup_asset_detail.page.dart | 4 + .../backup/drift_backup_options.page.dart | 2 +- .../lib/pages/common/app_log_detail.page.dart | 4 + .../lib/pages/common/splash_screen.page.dart | 85 ++++++++++--------- .../pages/library/locked/pin_auth.page.dart | 22 ++--- .../presentation/actions/action.widget.dart | 4 + .../pages/drift_album_options.page.dart | 24 ++++-- .../pages/drift_create_album.page.dart | 12 ++- .../pages/drift_partner_detail.page.dart | 5 +- .../pages/drift_remote_album.page.dart | 66 +++++++++----- .../pages/edit/drift_edit.page.dart | 10 ++- .../profile/profile_picture_crop.page.dart | 8 +- .../pages/search/drift_search.page.dart | 4 + .../add_action_button.widget.dart | 4 +- .../archive_action_button.widget.dart | 19 +++-- .../delete_action_button.widget.dart | 18 ++-- .../delete_local_action_button.widget.dart | 19 +++-- ...delete_permanent_action_button.widget.dart | 18 ++-- .../delete_trash_action_button.widget.dart | 18 ++-- .../edit_date_time_action_button.widget.dart | 18 ++-- .../edit_location_action_button.widget.dart | 18 ++-- .../favorite_action_button.widget.dart | 18 ++-- ...e_to_lock_folder_action_button.widget.dart | 18 ++-- ...emove_from_album_action_button.widget.dart | 18 ++-- ...from_lock_folder_action_button.widget.dart | 18 ++-- .../restore_action_button.widget.dart | 19 +++-- .../restore_trash_action_button.widget.dart | 18 ++-- .../set_album_cover.widget.dart | 18 ++-- .../stack_action_button.widget.dart | 18 ++-- .../trash_action_button.widget.dart | 18 ++-- .../unarchive_action_button.widget.dart | 19 +++-- .../unfavorite_action_button.widget.dart | 18 ++-- .../unstack_action_button.widget.dart | 18 ++-- .../widgets/album/album_selector.widget.dart | 10 ++- .../date_time_details.widget.dart | 4 + .../favorite_bottom_sheet.widget.dart | 3 + .../presentation/widgets/map/map_utils.dart | 10 ++- .../person_edit_birthday_modal.widget.dart | 6 +- .../people/person_edit_name_modal.widget.dart | 6 +- .../widgets/timeline/fixed/segment.model.dart | 4 + .../repositories/asset_media.repository.dart | 2 +- mobile/lib/services/action.service.dart | 8 ++ .../lib/services/immich_logger.service.dart | 4 + mobile/lib/utils/map_utils.dart | 10 ++- .../widgets/activities/comment_bubble.dart | 4 + .../common/app_bar_dialog/app_bar_dialog.dart | 8 ++ .../lib/widgets/common/date_time_picker.dart | 2 +- .../widgets/forms/change_password_form.dart | 29 ++++--- .../lib/widgets/forms/login/login_form.dart | 44 ++++++++++ .../widgets/forms/pin_registration_form.dart | 4 + .../widgets/settings/advanced_settings.dart | 6 +- .../sync_status_and_actions.dart | 39 ++++++--- .../widgets/settings/language_settings.dart | 4 + .../local_network_preference.dart | 3 + .../networking_settings.dart | 12 +++ .../primary_color_setting.dart | 4 + 59 files changed, 552 insertions(+), 309 deletions(-) diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 3f5a33b2b2..e828760789 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -65,7 +65,7 @@ linter: avoid_type_to_string: true # Flutter specific - use_build_context_synchronously: false + use_build_context_synchronously: true sized_box_for_whitespace: true use_colored_box: true use_decorated_box: true diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 58e93891a2..317733f1de 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -147,20 +147,9 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve Future initApp() async { WidgetsBinding.instance.addObserver(this); - // Draw the app from edge to edge unawaited(SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge)); - - // Sets the navigation bar color - SystemUiOverlayStyle overlayStyle = const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent); - if (Platform.isAndroid) { - // Android 8 does not support transparent app bars - final info = await DeviceInfoPlugin().androidInfo; - if (info.version.sdkInt <= 26) { - overlayStyle = context.isDarkTheme ? SystemUiOverlayStyle.dark : SystemUiOverlayStyle.light; - } - } - SystemChrome.setSystemUIOverlayStyle(overlayStyle); + await _setNavigationBarColor(); await FlutterLocalNotificationsPlugin().initialize( const InitializationSettings( @@ -170,6 +159,22 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve ); } + Future _setNavigationBarColor() async { + SystemUiOverlayStyle overlayStyle = const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent); + if (Platform.isAndroid) { + // Android 8 does not support transparent app bars + final info = await DeviceInfoPlugin().androidInfo; + if (!mounted) { + return; + } + + if (info.version.sdkInt <= 26) { + overlayStyle = context.isDarkTheme ? SystemUiOverlayStyle.dark : SystemUiOverlayStyle.light; + } + } + SystemChrome.setSystemUIOverlayStyle(overlayStyle); + } + Future _deepLinkBuilder(PlatformDeepLink deepLink) async { final deepLinkHandler = ref.read(deepLinkServiceProvider); final currentRouteName = ref.read(currentRouteNameProvider.notifier).state; diff --git a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart index 396f4224a7..7667dbc3f1 100644 --- a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart @@ -132,6 +132,10 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState { return; } - if (mounted) { - setState(() => _cleared = true); + if (!mounted) { + return; } + + setState(() => _cleared = true); } @override @@ -312,46 +314,53 @@ class SplashScreenPageState extends ConsumerState { final viewIntentHandler = ref.read(viewIntentHandlerProvider); unawaited( - ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then( - (_) async { - try { - wsProvider.connect(); - unawaited(infoProvider.getServerInfo()); + ref + .read(authProvider.notifier) + .saveAuthInfo(accessToken: accessToken) + .then( + (_) async { + try { + wsProvider.connect(); + unawaited(infoProvider.getServerInfo()); - bool syncSuccess = false; - await Future.wait([ - backgroundManager.syncLocal(full: true), - backgroundManager.syncRemote().then((success) => syncSuccess = success), - ]); + bool syncSuccess = false; + await Future.wait([ + backgroundManager.syncLocal(full: true), + backgroundManager.syncRemote().then((success) => syncSuccess = success), + ]); - await viewIntentHandler.flushDeferredViewIntent(); + await viewIntentHandler.flushDeferredViewIntent(); - if (syncSuccess) { - await Future.wait([ - backgroundManager.hashAssets().then((_) { - unawaited(_resumeBackup(backupProvider)); - }), - _resumeBackup(backupProvider), - // TODO: Bring back when the soft freeze issue is addressed - // backgroundManager.syncCloudIds(), - ]); - } else { - await backgroundManager.hashAssets(); - } + if (syncSuccess) { + await Future.wait([ + backgroundManager.hashAssets().then((_) { + unawaited(_resumeBackup(backupProvider)); + }), + _resumeBackup(backupProvider), + // TODO: Bring back when the soft freeze issue is addressed + // backgroundManager.syncCloudIds(), + ]); + } else { + await backgroundManager.hashAssets(); + } - if (SettingsRepository.instance.appConfig.backup.syncAlbums) { - await backgroundManager.syncLinkedAlbum(); - } - } catch (e) { - log.severe('Failed establishing connection to the server: $e'); - } - }, - onError: (exception) => { - log.severe('Failed to update auth info with access token: $accessToken'), - ref.read(authProvider.notifier).logout(), - context.router.replaceAll([const LoginRoute()]), - }, - ), + if (SettingsRepository.instance.appConfig.backup.syncAlbums) { + await backgroundManager.syncLinkedAlbum(); + } + } catch (e) { + log.severe('Failed establishing connection to the server: $e'); + } + }, + onError: (exception) { + log.severe('Failed to update auth info with access token: $accessToken'); + unawaited(ref.read(authProvider.notifier).logout()); + if (!mounted) { + return; + } + + unawaited(context.router.replaceAll([const LoginRoute()])); + }, + ), ); } else { log.severe('Missing crucial offline login info - Logging out completely'); diff --git a/mobile/lib/pages/library/locked/pin_auth.page.dart b/mobile/lib/pages/library/locked/pin_auth.page.dart index 2da9a8ddab..81e08d853b 100644 --- a/mobile/lib/pages/library/locked/pin_auth.page.dart +++ b/mobile/lib/pages/library/locked/pin_auth.page.dart @@ -25,17 +25,19 @@ class PinAuthPage extends HookConsumerWidget { Future registerBiometric(String pinCode) async { final isRegistered = await ref.read(localAuthProvider.notifier).registerBiometric(context, pinCode); - if (isRegistered) { - context.showSnackBar( - SnackBar( - content: Text('biometric_auth_enabled'.tr(), style: context.textTheme.labelLarge), - duration: const Duration(seconds: 3), - backgroundColor: context.colorScheme.primaryContainer, - ), - ); - - unawaited(context.replaceRoute(const DriftLockedFolderRoute())); + if (!isRegistered || !context.mounted) { + return; } + + context.showSnackBar( + SnackBar( + content: Text('biometric_auth_enabled'.tr(), style: context.textTheme.labelLarge), + duration: const Duration(seconds: 3), + backgroundColor: context.colorScheme.primaryContainer, + ), + ); + + unawaited(context.replaceRoute(const DriftLockedFolderRoute())); } Future enableBiometricAuth() { diff --git a/mobile/lib/presentation/actions/action.widget.dart b/mobile/lib/presentation/actions/action.widget.dart index eba5e3939c..f96dc2bc2f 100644 --- a/mobile/lib/presentation/actions/action.widget.dart +++ b/mobile/lib/presentation/actions/action.widget.dart @@ -23,6 +23,10 @@ class _ActionWidget extends ConsumerWidget { try { await action.onAction(scope); } catch (error, stackTrace) { + if (!scope.context.mounted) { + return; + } + handleError(scope.context, stack: stackTrace, description: 'Action failed: ${action.runtimeType}'); } } diff --git a/mobile/lib/presentation/pages/drift_album_options.page.dart b/mobile/lib/presentation/pages/drift_album_options.page.dart index 37c0273fae..9c5161fa8a 100644 --- a/mobile/lib/presentation/pages/drift_album_options.page.dart +++ b/mobile/lib/presentation/pages/drift_album_options.page.dart @@ -46,6 +46,10 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { Future leaveAlbum() async { try { await ref.read(remoteAlbumProvider.notifier).leaveAlbum(album.id, userId: userId); + if (!context.mounted) { + return; + } + unawaited(context.navigateTo(const DriftAlbumsRoute())); } catch (_) { showErrorMessage(); @@ -72,17 +76,21 @@ class DriftAlbumOptionsPage extends HookConsumerWidget { try { await ref.read(remoteAlbumProvider.notifier).addUsers(album.id, newUsers); - - if (newUsers.isNotEmpty) { - ImmichToast.show( - context: context, - msg: "users_added_to_album_count".t(context: context, args: {'count': newUsers.length}), - toastType: ToastType.success, - ); + ref.invalidate(remoteAlbumSharedUsersProvider(album.id)); + if (!context.mounted) { + return; } - ref.invalidate(remoteAlbumSharedUsersProvider(album.id)); + ImmichToast.show( + context: context, + msg: "users_added_to_album_count".t(context: context, args: {'count': newUsers.length}), + toastType: ToastType.success, + ); } catch (e) { + if (!context.mounted) { + return; + } + ImmichToast.show(context: context, msg: "Failed to add users to album: $e", toastType: ToastType.error); } } diff --git a/mobile/lib/presentation/pages/drift_create_album.page.dart b/mobile/lib/presentation/pages/drift_create_album.page.dart index 0dfae062dc..93534d62a5 100644 --- a/mobile/lib/presentation/pages/drift_create_album.page.dart +++ b/mobile/lib/presentation/pages/drift_create_album.page.dart @@ -186,13 +186,17 @@ class _DriftCreateAlbumPageState extends ConsumerState { assets: selectedAssets, ); - if (album != null && context.mounted) { - unawaited(context.replaceRoute(RemoteAlbumRoute(album: album))); + if (!mounted || album == null) { + return; } + + unawaited(context.replaceRoute(RemoteAlbumRoute(album: album))); } catch (_) { - if (context.mounted) { - ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.t()); + if (!mounted) { + return; } + + ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.t()); } finally { if (mounted) { setState(() => isCreatingAlbum = false); diff --git a/mobile/lib/presentation/pages/drift_partner_detail.page.dart b/mobile/lib/presentation/pages/drift_partner_detail.page.dart index 53353ce689..70de434d9f 100644 --- a/mobile/lib/presentation/pages/drift_partner_detail.page.dart +++ b/mobile/lib/presentation/pages/drift_partner_detail.page.dart @@ -72,13 +72,16 @@ class _InfoBoxState extends ConsumerState<_InfoBox> { }); } catch (error, stack) { dPrint(() => "Failed to toggle in timeline: $error $stack"); + if (!mounted) { + return; + } + ImmichToast.show( context: context, toastType: ToastType.error, durationInSecond: 1, msg: "Failed to toggle the timeline setting", ); - return; } } diff --git a/mobile/lib/presentation/pages/drift_remote_album.page.dart b/mobile/lib/presentation/pages/drift_remote_album.page.dart index 5e4e525e06..5d018561cc 100644 --- a/mobile/lib/presentation/pages/drift_remote_album.page.dart +++ b/mobile/lib/presentation/pages/drift_remote_album.page.dart @@ -42,6 +42,9 @@ class _RemoteAlbumPageState extends ConsumerState { Future addAssets(BuildContext context) async { final notifier = ref.read(remoteAlbumProvider.notifier); final albumAssets = await notifier.getAssets(_album.id); + if (!context.mounted) { + return; + } final newAssets = await context.pushRoute>( DriftAssetSelectionTimelineRoute(lockedSelectionAssets: albumAssets.toSet()), @@ -52,8 +55,11 @@ class _RemoteAlbumPageState extends ConsumerState { } final added = await notifier.addAssetsToAlbum(_album.id, newAssets); + if (!context.mounted) { + return; + } - if (added > 0 && context.mounted) { + if (added > 0) { ImmichToast.show( context: context, msg: "assets_added_to_album_count".t(context: context, args: {'count': added.toString()}), @@ -71,17 +77,21 @@ class _RemoteAlbumPageState extends ConsumerState { try { await ref.read(remoteAlbumProvider.notifier).addUsers(_album.id, newUsers); - - if (newUsers.isNotEmpty) { - ImmichToast.show( - context: context, - msg: "users_added_to_album_count".t(context: context, args: {'count': newUsers.length}), - toastType: ToastType.success, - ); + ref.invalidate(remoteAlbumSharedUsersProvider(_album.id)); + if (!context.mounted) { + return; } - ref.invalidate(remoteAlbumSharedUsersProvider(_album.id)); + ImmichToast.show( + context: context, + msg: "users_added_to_album_count".t(context: context, args: {'count': newUsers.length}), + toastType: ToastType.success, + ); } catch (e) { + if (!context.mounted) { + return; + } + ImmichToast.show(context: context, msg: "Failed to add users to album: $e", toastType: ToastType.error); } } @@ -124,6 +134,9 @@ class _RemoteAlbumPageState extends ConsumerState { if (confirmed == true) { try { await ref.read(remoteAlbumProvider.notifier).deleteAlbum(_album.id); + if (!context.mounted) { + return; + } ImmichToast.show( context: context, @@ -133,6 +146,10 @@ class _RemoteAlbumPageState extends ConsumerState { unawaited(context.pushRoute(const DriftAlbumsRoute())); } catch (e) { + if (!context.mounted) { + return; + } + ImmichToast.show( context: context, msg: 'album_viewer_appbar_share_err_delete'.t(context: context), @@ -149,7 +166,11 @@ class _RemoteAlbumPageState extends ConsumerState { builder: (context) => _EditAlbumDialog(album: _album), ); - if (result != null && context.mounted) { + if (!context.mounted) { + return; + } + + if (result != null) { setState(() { _album = _album.copyWith(name: result.name, description: result.description ?? ''); }); @@ -247,20 +268,23 @@ class _EditAlbumDialogState extends ConsumerState<_EditAlbumDialog> { await ref .read(remoteAlbumProvider.notifier) .updateAlbum(widget.album.id, name: newTitle, description: newDescription); + if (!mounted) { + return; + } - if (mounted) { - Navigator.of( - context, - ).pop(_EditAlbumData(name: newTitle, description: newDescription.isEmpty ? null : newDescription)); - } + Navigator.of( + context, + ).pop(_EditAlbumData(name: newTitle, description: newDescription.isEmpty ? null : newDescription)); } catch (e) { - if (mounted) { - ImmichToast.show( - context: context, - msg: 'album_update_error'.t(context: context), - toastType: ToastType.error, - ); + if (!mounted) { + return; } + + ImmichToast.show( + context: context, + msg: 'album_update_error'.t(context: context), + toastType: ToastType.error, + ); } } diff --git a/mobile/lib/presentation/pages/edit/drift_edit.page.dart b/mobile/lib/presentation/pages/edit/drift_edit.page.dart index 0ce9985c19..ec7d8ac3d7 100644 --- a/mobile/lib/presentation/pages/edit/drift_edit.page.dart +++ b/mobile/lib/presentation/pages/edit/drift_edit.page.dart @@ -59,9 +59,17 @@ class _DriftEditImagePageState extends ConsumerState with Ti try { await widget.applyEdits(edits); + if (!mounted) { + return; + } + ImmichToast.show(context: context, msg: 'success'.tr(), toastType: ToastType.success); Navigator.of(context).pop(); } catch (e) { + if (!mounted) { + return; + } + ImmichToast.show(context: context, msg: 'error_title'.tr(), toastType: ToastType.error); } finally { ref.read(editorStateProvider.notifier).setIsEditing(false); @@ -99,7 +107,7 @@ class _DriftEditImagePageState extends ConsumerState with Ti return; } final shouldDiscard = await _showDiscardChangesDialog() ?? false; - if (shouldDiscard && mounted) { + if (shouldDiscard && context.mounted) { Navigator.of(context).pop(); } }, diff --git a/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart b/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart index e6ae6ad44f..a987c9ca29 100644 --- a/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart +++ b/mobile/lib/presentation/pages/profile/profile_picture_crop.page.dart @@ -74,7 +74,7 @@ class _ProfilePictureCropPageState extends ConsumerState .read(uploadProfileImageProvider.notifier) .upload(xFile, fileName: 'profile-picture.png'); - if (!context.mounted) { + if (!mounted) { return; } @@ -94,9 +94,7 @@ class _ProfilePictureCropPageState extends ConsumerState toastType: ToastType.success, ); - if (context.mounted) { - unawaited(context.maybePop()); - } + unawaited(context.maybePop()); } else { ImmichToast.show( context: context, @@ -106,7 +104,7 @@ class _ProfilePictureCropPageState extends ConsumerState ); } } catch (e) { - if (!context.mounted) { + if (!mounted) { return; } diff --git a/mobile/lib/presentation/pages/search/drift_search.page.dart b/mobile/lib/presentation/pages/search/drift_search.page.dart index 8d6122804a..39588cf051 100644 --- a/mobile/lib/presentation/pages/search/drift_search.page.dart +++ b/mobile/lib/presentation/pages/search/drift_search.page.dart @@ -107,6 +107,10 @@ class DriftSearchPage extends HookConsumerWidget { unawaited( Future.microtask(() { + if (!context.mounted) { + return; + } + textSearchController.clear(); peopleCurrentFilterWidget.value = null; dateRangeCurrentFilterWidget.value = null; diff --git a/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart index bcd3b20df6..533bc72971 100644 --- a/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart @@ -144,7 +144,7 @@ class _AddActionButtonState extends ConsumerState { final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.viewer, album); - if (!context.mounted) { + if (!mounted) { return; } @@ -175,7 +175,7 @@ class _AddActionButtonState extends ConsumerState { ); } - if (!context.mounted) { + if (!mounted) { return; } await Navigator.of(context).maybePop(); diff --git a/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart index bb2cae21ad..3322dd3a85 100644 --- a/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart @@ -23,16 +23,17 @@ Future performArchiveAction(BuildContext context, WidgetRef ref, {required final result = await ref.read(actionProvider.notifier).archive(source); ref.read(multiSelectProvider.notifier).reset(); - final successMessage = 'archive_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); + if (!context.mounted) { + return; } + + final successMessage = 'archive_action_prompt'.t(context: context, args: {'count': result.count.toString()}); + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); } class ArchiveActionButton extends ConsumerWidget { diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart index 45dc5ec699..a6ed4c4246 100644 --- a/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart @@ -73,17 +73,17 @@ class DeleteActionButton extends ConsumerWidget { shouldRefreshStack ? ViewerStackAssetDeletedEvent(stackIndex: stackIndex) : const ViewerReloadAssetEvent(), ); } + if (!context.mounted) { + return; + } final successMessage = 'delete_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); } @override diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart index 5a94d9807e..09969d8b8a 100644 --- a/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart @@ -42,16 +42,17 @@ class DeleteLocalActionButton extends ConsumerWidget { ref.invalidate(localAlbumProvider); - final successMessage = 'delete_local_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); + if (!context.mounted) { + return; } + + final successMessage = 'delete_local_action_prompt'.t(context: context, args: {'count': result.count.toString()}); + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); } @override diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart index 922f8593fa..c02fcf8f79 100644 --- a/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart @@ -50,20 +50,20 @@ class DeletePermanentActionButton extends ConsumerWidget { final result = await ref.read(actionProvider.notifier).deleteRemoteAndLocal(source); ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } final successMessage = 'delete_permanently_action_prompt'.t( context: context, args: {'count': result.count.toString()}, ); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); } @override diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart index f3e048f06f..3312e4f2a3 100644 --- a/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart @@ -37,20 +37,20 @@ class DeleteTrashActionButton extends ConsumerWidget { final result = await ref.read(actionProvider.notifier).deleteRemoteAndLocal(source); ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } final successMessage = 'assets_permanently_deleted_count'.t( context: context, args: {'count': result.count.toString()}, ); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); } @override diff --git a/mobile/lib/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart index b2b5050a8e..e93720186b 100644 --- a/mobile/lib/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart @@ -24,20 +24,20 @@ class EditDateTimeActionButton extends ConsumerWidget { } ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } final successMessage = 'edit_date_and_time_action_prompt'.t( context: context, args: {'count': result.count.toString()}, ); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); } @override diff --git a/mobile/lib/presentation/widgets/action_buttons/edit_location_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/edit_location_action_button.widget.dart index cc8e15617c..b250e325ce 100644 --- a/mobile/lib/presentation/widgets/action_buttons/edit_location_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/edit_location_action_button.widget.dart @@ -24,17 +24,17 @@ class EditLocationActionButton extends ConsumerWidget { } ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } final successMessage = 'edit_location_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); } @override diff --git a/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart index 0365335fd2..0f3ce47122 100644 --- a/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart @@ -35,17 +35,17 @@ class FavoriteActionButton extends ConsumerWidget { } ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } final successMessage = 'favorite_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); } @override diff --git a/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart index 56191e9055..6d5f5b387a 100644 --- a/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart @@ -22,20 +22,20 @@ Future performMoveToLockFolderAction(BuildContext context, WidgetRef ref, final result = await ref.read(actionProvider.notifier).moveToLockFolder(source); ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } final successMessage = 'move_to_lock_folder_action_prompt'.t( context: context, args: {'count': result.count.toString()}, ); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); } class MoveToLockFolderActionButton extends ConsumerWidget { diff --git a/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart index ebcfbaa1e5..7049da13f8 100644 --- a/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart @@ -35,20 +35,20 @@ class RemoveFromAlbumActionButton extends ConsumerWidget { final result = await ref.read(actionProvider.notifier).removeFromAlbum(source, albumId); ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } final successMessage = 'remove_from_album_action_prompt'.t( context: context, args: {'count': result.count.toString()}, ); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); } @override diff --git a/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart index 75deef9ccb..ea0d9f384f 100644 --- a/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart @@ -27,20 +27,20 @@ class RemoveFromLockFolderActionButton extends ConsumerWidget { final result = await ref.read(actionProvider.notifier).removeFromLockFolder(source); ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } final successMessage = 'remove_from_lock_folder_action_prompt'.t( context: context, args: {'count': result.count.toString()}, ); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); } @override diff --git a/mobile/lib/presentation/widgets/action_buttons/restore_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/restore_action_button.widget.dart index b752a77c89..9270ce8351 100644 --- a/mobile/lib/presentation/widgets/action_buttons/restore_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/restore_action_button.widget.dart @@ -29,16 +29,17 @@ class RestoreActionButton extends ConsumerWidget { EventStream.shared.emit(const ViewerReloadAssetEvent()); } - final successMessage = 'assets_restored_count'.t(context: context, args: {'count': result.count.toString()}); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); + if (!context.mounted) { + return; } + + final successMessage = 'assets_restored_count'.t(context: context, args: {'count': result.count.toString()}); + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); } @override diff --git a/mobile/lib/presentation/widgets/action_buttons/restore_trash_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/restore_trash_action_button.widget.dart index 82a9d98549..f6cd26c189 100644 --- a/mobile/lib/presentation/widgets/action_buttons/restore_trash_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/restore_trash_action_button.widget.dart @@ -19,17 +19,17 @@ class RestoreTrashActionButton extends ConsumerWidget { final result = await ref.read(actionProvider.notifier).restoreTrash(source); ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } final successMessage = 'assets_restored_count'.t(context: context, args: {'count': result.count.toString()}); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); } @override diff --git a/mobile/lib/presentation/widgets/action_buttons/set_album_cover.widget.dart b/mobile/lib/presentation/widgets/action_buttons/set_album_cover.widget.dart index d080efc5b2..e6e572110e 100644 --- a/mobile/lib/presentation/widgets/action_buttons/set_album_cover.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/set_album_cover.widget.dart @@ -29,17 +29,17 @@ class SetAlbumCoverActionButton extends ConsumerWidget { final result = await ref.read(actionProvider.notifier).setAlbumCover(source, albumId); ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } final successMessage = 'album_cover_updated'.t(context: context); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); } @override diff --git a/mobile/lib/presentation/widgets/action_buttons/stack_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/stack_action_button.widget.dart index b87d288a3e..026268fe52 100644 --- a/mobile/lib/presentation/widgets/action_buttons/stack_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/stack_action_button.widget.dart @@ -26,17 +26,17 @@ class StackActionButton extends ConsumerWidget { final result = await ref.read(actionProvider.notifier).stack(user.id, source); ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } final successMessage = 'stack_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); } @override diff --git a/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart index a320d3b1b1..2be2049a07 100644 --- a/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart @@ -31,17 +31,17 @@ class TrashActionButton extends ConsumerWidget { final result = await ref.read(actionProvider.notifier).trash(source); ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } final successMessage = 'trash_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); } @override diff --git a/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart index 78984f9ef1..552608f83f 100644 --- a/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart @@ -25,16 +25,17 @@ Future performUnArchiveAction(BuildContext context, WidgetRef ref, {requir final result = await ref.read(actionProvider.notifier).unArchive(source); ref.read(multiSelectProvider.notifier).reset(); - final successMessage = 'unarchive_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); + if (!context.mounted) { + return; } + + final successMessage = 'unarchive_action_prompt'.t(context: context, args: {'count': result.count.toString()}); + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); } class UnArchiveActionButton extends ConsumerWidget { diff --git a/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart index 94d6588074..be6c3b0180 100644 --- a/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart @@ -35,17 +35,17 @@ class UnFavoriteActionButton extends ConsumerWidget { } ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } final successMessage = 'unfavorite_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); } @override diff --git a/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart index c9a5102a9b..47cdfe9b5f 100644 --- a/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart @@ -22,17 +22,17 @@ class UnStackActionButton extends ConsumerWidget { final result = await ref.read(actionProvider.notifier).unStack(source); ref.read(multiSelectProvider.notifier).reset(); + if (!context.mounted) { + return; + } final successMessage = 'unstack_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - - if (context.mounted) { - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } + ImmichToast.show( + context: context, + msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), + gravity: ToastGravity.BOTTOM, + toastType: result.success ? ToastType.success : ToastType.error, + ); } @override diff --git a/mobile/lib/presentation/widgets/album/album_selector.widget.dart b/mobile/lib/presentation/widgets/album/album_selector.widget.dart index bf5de5611d..f30796e406 100644 --- a/mobile/lib/presentation/widgets/album/album_selector.widget.dart +++ b/mobile/lib/presentation/widgets/album/album_selector.widget.dart @@ -755,6 +755,10 @@ class AddToAlbumHeader extends ConsumerWidget { .read(remoteAlbumProvider.notifier) .createAlbumWithAssets(title: albumName, assets: selectedAssets); + if (!context.mounted) { + return; + } + if (newAlbum == null) { ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.tr()); return; @@ -798,7 +802,7 @@ class CreateAlbumButton extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { Future onCreateAlbum() async { final albumName = await showDialog(context: context, builder: (context) => const NewAlbumNameModal()); - if (albumName == null) { + if (albumName == null || !context.mounted) { return; } @@ -813,6 +817,10 @@ class CreateAlbumButton extends ConsumerWidget { .read(remoteAlbumProvider.notifier) .createAlbum(title: albumName, assetIds: [asset.remoteId!]); + if (!context.mounted) { + return; + } + if (album == null) { ImmichToast.show(context: context, toastType: ToastType.error, msg: 'errors.failed_to_create_album'.tr()); return; diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/date_time_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/date_time_details.widget.dart index 27bac68310..2dc1c40456 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/date_time_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/date_time_details.widget.dart @@ -1,4 +1,5 @@ import 'dart:async'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -93,6 +94,9 @@ class _SheetAssetDescriptionState extends ConsumerState<_SheetAssetDescription> if (!editAction.success) { _controller.text = previousDescription ?? ''; + if (!mounted) { + return; + } ImmichToast.show( context: context, diff --git a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart index bcb9fc6fe3..4382eeba5d 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart @@ -44,6 +44,9 @@ class FavoriteBottomSheet extends ConsumerWidget { final result = await ref .read(remoteAlbumProvider.notifier) .addAssets(album.id, remoteAssets.map((e) => e.id).toList()); + if (!context.mounted) { + return; + } if (selectedAssets.length != remoteAssets.length) { ImmichToast.show( diff --git a/mobile/lib/presentation/widgets/map/map_utils.dart b/mobile/lib/presentation/widgets/map/map_utils.dart index 3ce7b2e055..34116b144b 100644 --- a/mobile/lib/presentation/widgets/map/map_utils.dart +++ b/mobile/lib/presentation/widgets/map/map_utils.dart @@ -71,7 +71,11 @@ class MapUtils { bool silent = false, }) async { try { - final bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); + final serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!context.mounted) { + return (null, LocationPermission.unableToDetermine); + } + if (!serviceEnabled && !silent) { unawaited(showDialog(context: context, builder: (context) => _LocationServiceDisabledDialog(context))); return (null, LocationPermission.deniedForever); @@ -81,6 +85,10 @@ class MapUtils { bool shouldRequestPermission = false; if (permission == LocationPermission.denied && !silent) { + if (!context.mounted) { + return (null, LocationPermission.unableToDetermine); + } + shouldRequestPermission = await showDialog( context: context, builder: (context) => _LocationPermissionDisabledDialog(context), diff --git a/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart b/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart index 6e66ff47ce..b5a9dd235c 100644 --- a/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_edit_birthday_modal.widget.dart @@ -34,12 +34,16 @@ class _DriftPersonNameEditFormState extends ConsumerState(_selectedDate); } } catch (error) { dPrint(() => 'Error updating birthday: $error'); - if (!context.mounted) { + if (!mounted) { return; } diff --git a/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart b/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart index 2eaac2ebf5..bb63de3695 100644 --- a/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart +++ b/mobile/lib/presentation/widgets/people/person_edit_name_modal.widget.dart @@ -32,12 +32,16 @@ class _DriftPersonNameEditFormState extends ConsumerState(newName); } } catch (error) { dPrint(() => 'Error updating name: $error'); - if (!context.mounted) { + if (!mounted) { return; } diff --git a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart index 7712983fde..16c947ca5e 100644 --- a/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart +++ b/mobile/lib/presentation/widgets/timeline/fixed/segment.model.dart @@ -211,6 +211,10 @@ class _AssetTileWidget extends ConsumerWidget { ref.read(multiSelectProvider.notifier).toggleAssetSelection(asset); } else { await ref.read(timelineServiceProvider).loadAssets(assetIndex, 1); + if (!ctx.mounted) { + return; + } + ref.read(isPlayingMotionVideoProvider.notifier).playing = false; AssetViewer.setAsset(ref, asset); unawaited( diff --git a/mobile/lib/repositories/asset_media.repository.dart b/mobile/lib/repositories/asset_media.repository.dart index 5bfb18a00f..6058883544 100644 --- a/mobile/lib/repositories/asset_media.repository.dart +++ b/mobile/lib/repositories/asset_media.repository.dart @@ -324,7 +324,7 @@ class AssetMediaRepository { return 0; } - if (_isCancelled(cancelCompleter)) { + if (_isCancelled(cancelCompleter) || !context.mounted) { await _cleanupTempFiles(tempFiles); return 0; } diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index 19782c8512..5986f0407d 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -157,6 +157,10 @@ class ActionService { } } + if (!context.mounted) { + return false; + } + final location = await showLocationPicker(context: context, initialLatLng: initialLatLng); if (location == null) { @@ -195,6 +199,10 @@ class ActionService { initialDate = dt; } + if (!context.mounted) { + return false; + } + final dateTime = await showDateTimePicker( context: context, initialDateTime: initialDate, diff --git a/mobile/lib/services/immich_logger.service.dart b/mobile/lib/services/immich_logger.service.dart index fab4b9966a..bfc9bd9b51 100644 --- a/mobile/lib/services/immich_logger.service.dart +++ b/mobile/lib/services/immich_logger.service.dart @@ -39,6 +39,10 @@ abstract final class ImmichLogger { await io.close(); } + if (!context.mounted) { + return; + } + final box = context.findRenderObject() as RenderBox?; // Share file diff --git a/mobile/lib/utils/map_utils.dart b/mobile/lib/utils/map_utils.dart index 19c66e51e9..d5e6f957a5 100644 --- a/mobile/lib/utils/map_utils.dart +++ b/mobile/lib/utils/map_utils.dart @@ -68,7 +68,11 @@ class MapUtils { bool silent = false, }) async { try { - final bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); + final serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!context.mounted) { + return (null, LocationPermission.unableToDetermine); + } + if (!serviceEnabled && !silent) { unawaited(showDialog(context: context, builder: (context) => _LocationServiceDisabledDialog())); return (null, LocationPermission.deniedForever); @@ -78,6 +82,10 @@ class MapUtils { bool shouldRequestPermission = false; if (permission == LocationPermission.denied && !silent) { + if (!context.mounted) { + return (null, LocationPermission.unableToDetermine); + } + shouldRequestPermission = await showDialog( context: context, builder: (context) => _LocationPermissionDisabledDialog(), diff --git a/mobile/lib/widgets/activities/comment_bubble.dart b/mobile/lib/widgets/activities/comment_bubble.dart index 95cff7b87d..fcaff8bfc3 100644 --- a/mobile/lib/widgets/activities/comment_bubble.dart +++ b/mobile/lib/widgets/activities/comment_bubble.dart @@ -35,6 +35,10 @@ class CommentBubble extends ConsumerWidget { Future openAssetViewer() async { final activityService = ref.read(activityServiceProvider); final route = await activityService.buildAssetViewerRoute(activity.assetId!, ref); + if (!context.mounted) { + return; + } + if (route != null) { await context.pushRoute(route); } diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart index 085f4e2120..53c1eb1af9 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart @@ -126,6 +126,10 @@ class ImmichAppBarDialog extends HookConsumerWidget { await ref.read(authProvider.notifier).logout().whenComplete(() => isLoggingOut.value = false); ref.read(websocketProvider.notifier).disconnect(); + if (!context.mounted) { + return; + } + unawaited(context.replaceRoute(const LoginRoute())); }, ); @@ -199,6 +203,10 @@ class ImmichAppBarDialog extends HookConsumerWidget { onTap: () async { ContextHelper(context).pop(); final packageInfo = await PackageInfo.fromPlatform(); + if (!context.mounted) { + return; + } + showLicensePage( context: context, applicationIcon: const Padding( diff --git a/mobile/lib/widgets/common/date_time_picker.dart b/mobile/lib/widgets/common/date_time_picker.dart index 679241fc1b..3aedd55de5 100644 --- a/mobile/lib/widgets/common/date_time_picker.dart +++ b/mobile/lib/widgets/common/date_time_picker.dart @@ -90,7 +90,7 @@ class _DateTimePicker extends HookWidget { firstDate: DateTime(1800), lastDate: now, ); - if (newDate == null) { + if (newDate == null || !context.mounted) { return; } diff --git a/mobile/lib/widgets/forms/change_password_form.dart b/mobile/lib/widgets/forms/change_password_form.dart index 7ab556b292..dd307f4ede 100644 --- a/mobile/lib/widgets/forms/change_password_form.dart +++ b/mobile/lib/widgets/forms/change_password_form.dart @@ -59,26 +59,29 @@ class ChangePasswordForm extends HookConsumerWidget { .read(authProvider.notifier) .changePassword(passwordController.value.text); - if (isSuccess) { - await ref.read(authProvider.notifier).logout(); - ref.read(websocketProvider.notifier).disconnect(); - - AutoRouter.of(context).back(); - - ImmichToast.show( - context: context, - msg: "login_password_changed_success".tr(), - toastType: ToastType.success, - gravity: ToastGravity.TOP, - ); - } else { + if (!isSuccess && context.mounted) { ImmichToast.show( context: context, msg: "login_password_changed_error".tr(), toastType: ToastType.error, gravity: ToastGravity.TOP, ); + return; } + + await ref.read(authProvider.notifier).logout(); + ref.read(websocketProvider.notifier).disconnect(); + if (!context.mounted) { + return; + } + + AutoRouter.of(context).back(); + ImmichToast.show( + context: context, + msg: "login_password_changed_success".tr(), + toastType: ToastType.success, + gravity: ToastGravity.TOP, + ); } }, ), diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index 969c311bfb..18963f0cfd 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -115,6 +115,10 @@ class LoginForm extends HookConsumerWidget { serverEndpoint.value = endpoint; } on ApiException catch (e) { + if (!context.mounted) { + return; + } + ImmichToast.show( context: context, msg: e.message ?? 'login_form_api_exception'.tr(), @@ -124,6 +128,10 @@ class LoginForm extends HookConsumerWidget { isOauthEnable.value = false; isPasswordLoginEnable.value = true; } on HandshakeException { + if (!context.mounted) { + return; + } + ImmichToast.show( context: context, msg: 'login_form_handshake_exception'.tr(), @@ -133,6 +141,10 @@ class LoginForm extends HookConsumerWidget { isOauthEnable.value = false; isPasswordLoginEnable.value = true; } catch (e) { + if (!context.mounted) { + return; + } + ImmichToast.show( context: context, msg: 'login_form_server_error'.tr(), @@ -180,6 +192,10 @@ class LoginForm extends HookConsumerWidget { Future getManageMediaPermission() async { final hasPermission = await ref.read(permissionRepositoryProvider).hasManageMediaPermission(); + if (!context.mounted) { + return; + } + if (!hasPermission) { await showDialog( context: context, @@ -236,6 +252,10 @@ class LoginForm extends HookConsumerWidget { try { final result = await ref.read(authProvider.notifier).login(emailController.text, passwordController.text); + if (!context.mounted) { + return; + } + if (result.shouldChangePassword && !result.isAdmin) { unawaited(context.pushRoute(const ChangePasswordRoute())); } else { @@ -246,10 +266,18 @@ class LoginForm extends HookConsumerWidget { unawaited(handleSyncFlow()); ref.read(websocketProvider.notifier).connect(); unawaited(ref.read(featureMessageServiceProvider).markSeen()); + if (!context.mounted) { + return; + } + unawaited(context.router.replaceAll([const TabShellRoute()])); return; } } catch (error) { + if (!context.mounted) { + return; + } + ImmichToast.show( context: context, msg: "login_form_failed_login".tr(), @@ -304,6 +332,10 @@ class LoginForm extends HookConsumerWidget { } catch (error, stack) { log.severe('Error getting OAuth server Url: $error', stack); + if (!context.mounted) { + return; + } + ImmichToast.show( context: context, msg: "login_form_failed_get_oauth_server_config".tr(), @@ -334,12 +366,20 @@ class LoginForm extends HookConsumerWidget { } unawaited(handleSyncFlow()); unawaited(ref.read(featureMessageServiceProvider).markSeen()); + if (!context.mounted) { + return; + } + unawaited(context.router.replaceAll([const TabShellRoute()])); return; } } catch (error, stack) { log.severe('Error logging in with OAuth: $error', stack); + if (!context.mounted) { + return; + } + ImmichToast.show( context: context, msg: error.toString(), @@ -348,6 +388,10 @@ class LoginForm extends HookConsumerWidget { ); } finally {} } else { + if (!context.mounted) { + return; + } + ImmichToast.show( context: context, msg: "login_form_failed_get_oauth_server_disable".tr(), diff --git a/mobile/lib/widgets/forms/pin_registration_form.dart b/mobile/lib/widgets/forms/pin_registration_form.dart index b3270ec524..ebd299bcce 100644 --- a/mobile/lib/widgets/forms/pin_registration_form.dart +++ b/mobile/lib/widgets/forms/pin_registration_form.dart @@ -42,6 +42,10 @@ class PinRegistrationForm extends HookConsumerWidget { onDone(); } catch (error) { hasError.value = true; + if (!context.mounted) { + return; + } + context.showSnackBar(SnackBar(content: Text(error.toString()))); } } diff --git a/mobile/lib/widgets/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index dbf54fd082..bf8673ccc5 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -145,6 +145,10 @@ class AdvancedSettings extends HookConsumerWidget { try { clearedBytes = await remoteImageApi.clearCache(); } catch (e) { + if (!context.mounted) { + return; + } + context.scaffoldMessenger.showSnackBar( SnackBar( duration: const Duration(seconds: 2), @@ -157,7 +161,7 @@ class AdvancedSettings extends HookConsumerWidget { return; } - if (clearedBytes < 0) { + if (clearedBytes < 0 || !context.mounted) { return; } diff --git a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart index 1871dc8a62..58a044cea4 100644 --- a/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart +++ b/mobile/lib/widgets/settings/beta_sync_settings/sync_status_and_actions.dart @@ -41,11 +41,13 @@ class SyncStatusAndActions extends HookConsumerWidget { // ignore: avoid_slow_async_io if (!await dbFile.exists()) { - if (context.mounted) { - context.scaffoldMessenger.showSnackBar( - SnackBar(content: Text("Database file not found".t(context: context))), - ); + if (!context.mounted) { + return; } + + context.scaffoldMessenger.showSnackBar( + SnackBar(content: Text("Database file not found".t(context: context))), + ); return; } @@ -54,6 +56,10 @@ class SyncStatusAndActions extends HookConsumerWidget { await dbFile.copy(exportFile.path); + if (!context.mounted) { + return; + } + final size = MediaQuery.of(context).size; await Share.shareXFiles( [XFile(exportFile.path)], @@ -67,18 +73,21 @@ class SyncStatusAndActions extends HookConsumerWidget { await exportFile.delete(); } }); + if (!context.mounted) { + return; + } - if (context.mounted) { - context.scaffoldMessenger.showSnackBar( - SnackBar(content: Text("Database exported successfully".t(context: context))), - ); - } + context.scaffoldMessenger.showSnackBar( + SnackBar(content: Text("Database exported successfully".t(context: context))), + ); } catch (e) { - if (context.mounted) { - context.scaffoldMessenger.showSnackBar( - SnackBar(content: Text("Failed to export database: $e".t(context: context))), - ); + if (!context.mounted) { + return; } + + context.scaffoldMessenger.showSnackBar( + SnackBar(content: Text("Failed to export database: $e".t(context: context))), + ); } } @@ -98,6 +107,10 @@ class SyncStatusAndActions extends HookConsumerWidget { TextButton( onPressed: () async { await ref.read(driftProvider).reset(); + if (!context.mounted) { + return; + } + context.pop(); unawaited( showDialog( diff --git a/mobile/lib/widgets/settings/language_settings.dart b/mobile/lib/widgets/settings/language_settings.dart index 2482801923..b12d1476f5 100644 --- a/mobile/lib/widgets/settings/language_settings.dart +++ b/mobile/lib/widgets/settings/language_settings.dart @@ -21,6 +21,10 @@ class LanguageSettings extends HookConsumerWidget { isLoading.value = true; await Future.delayed(const Duration(milliseconds: 500)); try { + if (!context.mounted) { + return; + } + await context.setLocale(selectedLocale.value); await loadTranslations(); } finally { diff --git a/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart b/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart index f8b6b087a3..8b787bcaea 100644 --- a/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart +++ b/mobile/lib/widgets/settings/networking_settings/local_network_preference.dart @@ -96,6 +96,9 @@ class LocalNetworkPreference extends HookConsumerWidget { Future autofillCurrentNetwork() async { final wifiName = await ref.read(networkProvider.notifier).getWifiName(); + if (!context.mounted) { + return; + } if (wifiName == null) { context.showSnackBar( diff --git a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart index e7510053e3..513648f030 100644 --- a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart +++ b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart @@ -46,6 +46,10 @@ class NetworkingSettings extends HookConsumerWidget { onPressed: () async { final isGrant = await ref.read(networkProvider.notifier).requestWifiReadPermission(); + if (!context.mounted) { + return; + } + Navigator.pop(context, isGrant); }, child: Text("grant_permission".tr()), @@ -56,6 +60,10 @@ class NetworkingSettings extends HookConsumerWidget { ); } + if (!context.mounted) { + return; + } + if (!hasLocationAlways) { isGrantLocationAlwaysPermission = await showDialog( context: context, @@ -68,6 +76,10 @@ class NetworkingSettings extends HookConsumerWidget { onPressed: () async { final isGrant = await ref.read(networkProvider.notifier).requestWifiReadBackgroundPermission(); + if (!context.mounted) { + return; + } + Navigator.pop(context, isGrant); }, child: Text("grant_permission".tr()), diff --git a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart index b0624dca23..b1564b4145 100644 --- a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart @@ -23,6 +23,10 @@ class PrimaryColorSetting extends HookConsumerWidget { void popBottomSheet() { Future.delayed(const Duration(milliseconds: 200), () { + if (!context.mounted) { + return; + } + Navigator.pop(context); }); } From fe5c8ed0fb80a741a12338b6e14dede5d5add457 Mon Sep 17 00:00:00 2001 From: shenlong Date: Fri, 31 Jul 2026 02:32:14 +0530 Subject: [PATCH 23/81] refactor: base action (#29617) * refactor: existing actions to new structure # Conflicts: # mobile/lib/presentation/actions/action.widget.dart # mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart # mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart # mobile/test/unit/presentation/partner_page_test.dart * rename to actionitem * more cleanup * review changes * lint changes --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- mobile/lib/domain/services/asset.service.dart | 9 -- .../repositories/remote_asset.repository.dart | 12 -- .../pages/library/partner/partner.page.dart | 6 +- mobile/lib/presentation/actions/action.dart | 60 +++++++--- .../presentation/actions/action.widget.dart | 104 ++++++----------- .../actions/asset_debug.action.dart | 29 ++--- .../presentation/actions/favorite.action.dart | 72 ++++++++---- .../presentation/actions/partner.action.dart | 51 +++++---- .../presentation/actions/timeline.action.dart | 24 ---- .../favorite_action_button.widget.dart | 61 ---------- .../unfavorite_action_button.widget.dart | 61 ---------- .../viewer_top_app_bar.widget.dart | 3 +- .../archive_bottom_sheet.widget.dart | 6 +- .../favorite_bottom_sheet.widget.dart | 6 +- .../general_bottom_sheet.widget.dart | 11 +- .../remote_album_bottom_sheet.widget.dart | 6 +- .../infrastructure/action.provider.dart | 22 ---- .../infrastructure/toast.provider.dart | 4 +- mobile/lib/providers/user.provider.dart | 8 ++ .../repositories/asset_api.repository.dart | 5 - mobile/lib/services/action.service.dart | 10 -- .../toast.service.dart} | 4 +- mobile/lib/utils/action_button.utils.dart | 2 +- mobile/test/repository.mocks.dart | 3 - mobile/test/service.mocks.dart | 3 + mobile/test/unit/mocks.dart | 4 +- .../actions/asset_debug_action_test.dart | 12 +- .../actions/favorite_action_test.dart | 50 +++++--- .../actions/timeline_action_test.dart | 108 ------------------ .../unit/presentation/partner_page_test.dart | 12 +- .../presentation/presentation_context.dart | 12 +- 31 files changed, 252 insertions(+), 528 deletions(-) delete mode 100644 mobile/lib/presentation/actions/timeline.action.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart rename mobile/lib/{repositories/toast.repository.dart => services/toast.service.dart} (91%) delete mode 100644 mobile/test/unit/presentation/actions/timeline_action_test.dart diff --git a/mobile/lib/domain/services/asset.service.dart b/mobile/lib/domain/services/asset.service.dart index f4c4a519d3..f35c962ff0 100644 --- a/mobile/lib/domain/services/asset.service.dart +++ b/mobile/lib/domain/services/asset.service.dart @@ -171,13 +171,4 @@ class AssetService { Future getLocalAsset(String id) { return _localRepository.get(id); } - - Future updateFavorite(List remoteIds, bool isFavorite) async { - if (remoteIds.isEmpty) { - return; - } - - await _apiRepository.updateFavorite(remoteIds, isFavorite); - await _remoteRepository.updateFavorite(remoteIds, isFavorite); - } } diff --git a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart index cdf8bfa15b..e97b05465b 100644 --- a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart @@ -117,18 +117,6 @@ class RemoteAssetRepository extends DriftDatabaseRepository { }).get(); } - Future updateFavorite(List ids, bool isFavorite) { - return _db.batch((batch) async { - for (final id in ids) { - batch.update( - _db.remoteAssetEntity, - RemoteAssetEntityCompanion(isFavorite: Value(isFavorite)), - where: (e) => e.id.equals(id), - ); - } - }); - } - Future updateVisibility(List ids, AssetVisibility visibility) { return _db.batch((batch) async { for (final id in ids) { diff --git a/mobile/lib/pages/library/partner/partner.page.dart b/mobile/lib/pages/library/partner/partner.page.dart index 7274b8a14e..afe64d24f4 100644 --- a/mobile/lib/pages/library/partner/partner.page.dart +++ b/mobile/lib/pages/library/partner/partner.page.dart @@ -33,7 +33,7 @@ class PartnerPage extends ConsumerWidget { title: Text(context.t.partners), elevation: 0, centerTitle: false, - actions: const [ActionIconButtonWidget(action: PartnerAddAction())], + actions: const [ActionIconButton(action: PartnerAddAction())], ), body: sharedByAsync.when( data: (partners) => PartnerSharedByList(partners: partners.toList(growable: false)), @@ -60,7 +60,7 @@ class _EmptyPartners extends StatelessWidget { ), const Align( alignment: .center, - child: ActionButtonWidget(action: PartnerAddAction()), + child: ActionButton(action: PartnerAddAction()), ), ], ), @@ -88,7 +88,7 @@ class PartnerSharedByList extends StatelessWidget { leading: PartnerUserAvatar(userId: partner.id, name: partner.name), title: Text(partner.name), subtitle: Text(partner.email), - trailing: ActionIconButtonWidget( + trailing: ActionIconButton( action: PartnerRemoveAction(sharedWithId: partner.id, partnerName: partner.name), ), ); diff --git a/mobile/lib/presentation/actions/action.dart b/mobile/lib/presentation/actions/action.dart index 5ceb2f855d..072c2524be 100644 --- a/mobile/lib/presentation/actions/action.dart +++ b/mobile/lib/presentation/actions/action.dart @@ -1,32 +1,54 @@ +import 'dart:async'; + import 'package:flutter/material.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/user.model.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/utils/asset_filter.dart'; -class ActionScope { - final BuildContext context; - final WidgetRef ref; - final UserDto authUser; +class ActionItem { + final IconData icon; + final String label; + final FutureOr Function() onAction; + final FutureOr Function()? onSecondaryAction; - const ActionScope({required this.context, required this.ref, required this.authUser}); + const ActionItem({required this.icon, required this.label, required this.onAction, this.onSecondaryAction}); } -abstract class BaseAction { - const BaseAction(); +abstract class ActionBuilder { + const ActionBuilder(); - IconData get icon; - - String label(ActionScope scope); - - bool isVisible(ActionScope scope) => true; - - Future onAction(ActionScope scope); + // null when the action is not applicable for the current context + ActionItem? create(BuildContext context, WidgetRef ref); } -abstract class AssetAction extends BaseAction { - final Iterable assets; +final assetsActionProvider = Provider.family.autoDispose, ActionSource>( + (ref, source) => AssetFilter(switch (source) { + .timeline => ref.watch(multiSelectProvider.select((s) => s.selectedAssets)), + .viewer => switch (ref.watch(assetViewerProvider.select((s) => s.currentAsset))) { + final BaseAsset asset => {asset}, + null => const {}, + }, + }), +); - const AssetAction({required this.assets}); +final clearSelectionProvider = Provider.family.autoDispose((ref, source) { + if (source == .timeline) { + return ref.read(multiSelectProvider.notifier).reset; + } - Iterable filter(ActionScope scope) => assets.whereType(); + return () {}; +}); + +final ownedAssetsActionProvider = Provider.family.autoDispose, ActionSource>( + (ref, source) => ref.watch(assetsActionProvider(source)).owned(ref.watch(authUserProvider).id), +); + +abstract class AssetActionBuilder extends ActionBuilder { + final ActionSource source; + + const AssetActionBuilder({required this.source}); } diff --git a/mobile/lib/presentation/actions/action.widget.dart b/mobile/lib/presentation/actions/action.widget.dart index f96dc2bc2f..a865528440 100644 --- a/mobile/lib/presentation/actions/action.widget.dart +++ b/mobile/lib/presentation/actions/action.widget.dart @@ -1,99 +1,71 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/presentation/actions/action.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/utils/error_handler.dart'; import 'package:immich_ui/immich_ui.dart'; -class _ActionWidgetScope { - final String label; - final VoidCallback onAction; +abstract class ActionWidget extends ConsumerWidget { + final ActionBuilder action; - const _ActionWidgetScope({required this.label, required this.onAction}); -} + const ActionWidget({super.key, required this.action}); -class _ActionWidget extends ConsumerWidget { - final BaseAction action; - final Widget Function(_ActionWidgetScope context) builder; - - const _ActionWidget({required this.action, required this.builder}); - - Future _onAction(ActionScope scope) async { - try { - await action.onAction(scope); - } catch (error, stackTrace) { - if (!scope.context.mounted) { - return; - } - - handleError(scope.context, stack: stackTrace, description: 'Action failed: ${action.runtimeType}'); - } - } + Widget builder(BuildContext context, WidgetRef ref, ActionItem action); @override Widget build(BuildContext context, WidgetRef ref) { - final authUser = ref.watch(currentUserProvider); - if (authUser == null) { + final actionItem = action.create(context, ref); + if (actionItem == null) { return const SizedBox.shrink(); } - final scope = ActionScope(context: context, ref: ref, authUser: authUser); - if (!action.isVisible(scope)) { - return const SizedBox.shrink(); - } - - return builder(.new(label: action.label(scope), onAction: () => _onAction(scope))); + return builder(context, ref, actionItem); } } -class ActionIconButtonWidget extends StatelessWidget { - final BaseAction action; +class ActionColumnButton extends ActionWidget { + const ActionColumnButton({super.key, required super.action}); + + @override + Widget builder(BuildContext context, WidgetRef ref, ActionItem action) => ImmichColumnButton( + icon: action.icon, + label: action.label, + onPressed: action.onAction, + onLongPress: action.onSecondaryAction, + ); +} + +class ActionIconButton extends ActionWidget { final ImmichVariant variant; - const ActionIconButtonWidget({super.key, required this.action, this.variant = .ghost}); + const ActionIconButton({super.key, required super.action, this.variant = .ghost}); @override - Widget build(BuildContext context) => _ActionWidget( - action: action, - builder: (ctx) => ImmichIconButton(icon: action.icon, onPressed: ctx.onAction, variant: variant), + Widget builder(BuildContext context, WidgetRef ref, ActionItem action) => ImmichIconButton( + icon: action.icon, + onPressed: action.onAction, + onLongPress: action.onSecondaryAction, + variant: variant, ); } -class ActionButtonWidget extends StatelessWidget { - final BaseAction action; +class ActionButton extends ActionWidget { final ImmichVariant variant; - const ActionButtonWidget({super.key, required this.action, this.variant = .ghost}); + const ActionButton({super.key, required super.action, this.variant = .ghost}); @override - Widget build(BuildContext context) => _ActionWidget( - action: action, - builder: (ctx) => - ImmichTextButton(labelText: ctx.label, icon: action.icon, onPressed: ctx.onAction, variant: variant), + Widget builder(BuildContext context, WidgetRef ref, ActionItem action) => ImmichTextButton( + labelText: action.label, + icon: action.icon, + onPressed: action.onAction, + onLongPress: action.onSecondaryAction, + variant: variant, ); } -class ActionColumnButtonWidget extends StatelessWidget { - final BaseAction action; - - const ActionColumnButtonWidget({super.key, required this.action}); +class ActionMenuItem extends ActionWidget { + const ActionMenuItem({super.key, required super.action}); @override - Widget build(BuildContext context) => _ActionWidget( - action: action, - builder: (ctx) => ImmichColumnButton(icon: action.icon, label: ctx.label, onPressed: ctx.onAction), - ); -} - -class ActionMenuItemWidget extends StatelessWidget { - final BaseAction action; - - const ActionMenuItemWidget({super.key, required this.action}); - - @override - Widget build(BuildContext context) => _ActionWidget( - action: action, - builder: (ctx) => ImmichMenuItem(icon: action.icon, label: ctx.label, onPressed: ctx.onAction), - ); + Widget builder(BuildContext context, WidgetRef ref, ActionItem action) => + ImmichMenuItem(icon: action.icon, label: action.label, onPressed: action.onAction); } diff --git a/mobile/lib/presentation/actions/asset_debug.action.dart b/mobile/lib/presentation/actions/asset_debug.action.dart index aec99fc90b..ff2935fc96 100644 --- a/mobile/lib/presentation/actions/asset_debug.action.dart +++ b/mobile/lib/presentation/actions/asset_debug.action.dart @@ -2,26 +2,27 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/action.dart'; import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -class AssetDebugAction extends AssetAction { - const AssetDebugAction({required super.assets}); +class AssetDebugAction extends AssetActionBuilder { + const AssetDebugAction({required super.source}); @override - IconData get icon => Icons.help_outline_rounded; + ActionItem? create(BuildContext context, WidgetRef ref) { + final assets = ref.watch(assetsActionProvider(source)).assets; + final troubleshootEnabled = ref.watch(settingsProvider.notifier).get(.advancedTroubleshooting); + if (!troubleshootEnabled || assets.length != 1) { + return null; + } - @override - String label(ActionScope scope) => scope.context.t.troubleshoot; - - @override - bool isVisible(ActionScope scope) => - assets.length == 1 && scope.ref.watch(settingsProvider.notifier).get(.advancedTroubleshooting); - - @override - Future onAction(ActionScope scope) async => - unawaited(scope.context.pushRoute(AssetTroubleshootRoute(asset: assets.first))); + return .new( + icon: Icons.help_outline_rounded, + label: context.t.troubleshoot, + onAction: () => unawaited(context.pushRoute(AssetTroubleshootRoute(asset: assets.single))), + ); + } } diff --git a/mobile/lib/presentation/actions/favorite.action.dart b/mobile/lib/presentation/actions/favorite.action.dart index a480e10a5f..9ab3770163 100644 --- a/mobile/lib/presentation/actions/favorite.action.dart +++ b/mobile/lib/presentation/actions/favorite.action.dart @@ -1,38 +1,62 @@ import 'package:flutter/material.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/presentation/actions/action.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -import 'package:immich_mobile/utils/asset_filter.dart'; -import 'package:immich_ui/immich_ui.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; -class FavoriteAction extends AssetAction { - final bool favorite; +typedef _State = ({bool shouldFavorite, List assetIds}); - FavoriteAction({required super.assets}) : favorite = assets.any((asset) => !asset.isFavorite); +final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, source) { + final assets = ref.watch(ownedAssetsActionProvider(source)); + if (assets.isEmpty) { + return null; + } + + 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); +}); + +class FavoriteAction extends AssetActionBuilder { + const FavoriteAction({required super.source}); @override - IconData get icon => favorite ? Icons.favorite_border_rounded : Icons.favorite_rounded; + ActionItem? create(BuildContext context, WidgetRef ref) { + final shouldFavorite = ref.watch(_stateProvider(source).select((state) => state?.shouldFavorite)); + if (shouldFavorite == null) { + return null; + } - @override - String label(ActionScope scope) => favorite ? scope.context.t.favorite : scope.context.t.unfavorite; + return .new( + icon: shouldFavorite ? Icons.favorite_border_rounded : Icons.favorite_rounded, + label: shouldFavorite ? context.t.favorite : context.t.unfavorite, + onAction: () => _favorite(context, ref), + ); + } - @override - Iterable filter(ActionScope scope) => - AssetFilter(assets).owned(scope.authUser.id).favorite(isFavorite: !favorite); + Future _favorite(BuildContext context, WidgetRef ref) async { + final state = ref.read(_stateProvider(source)); + if (state == null) { + return; + } - @override - bool isVisible(ActionScope scope) => filter(scope).isNotEmpty; + final _State(:shouldFavorite, :assetIds) = state; + final message = shouldFavorite + ? context.t.favorite_action_prompt(count: assetIds.length) + : context.t.unfavorite_action_prompt(count: assetIds.length); + final assertService = ref.read(assetServiceProvider); + final toastService = ref.read(toastServiceProvider); + final clearSelection = ref.read(clearSelectionProvider(source)); - @override - Future onAction(ActionScope scope) async { - final ActionScope(:ref) = scope; - final assets = filter(scope).map((asset) => asset.id).toList(growable: false); - - await ref.read(assetServiceProvider).updateFavorite(assets, favorite); - final message = favorite - ? StaticTranslations.instance.favorite_action_prompt(count: assets.length) - : StaticTranslations.instance.unfavorite_action_prompt(count: assets.length); - snackbar.success(message); + try { + await assertService.update(assetIds, isFavorite: .some(shouldFavorite)); + toastService.success(message); + clearSelection(); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to update favorite status for assets"); + } } } diff --git a/mobile/lib/presentation/actions/partner.action.dart b/mobile/lib/presentation/actions/partner.action.dart index 11fb69ee75..012340c1d8 100644 --- a/mobile/lib/presentation/actions/partner.action.dart +++ b/mobile/lib/presentation/actions/partner.action.dart @@ -6,44 +6,46 @@ import 'package:immich_mobile/presentation/actions/action.dart'; import 'package:immich_mobile/presentation/widgets/people/partner_user_avatar.widget.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; -class PartnerAddAction extends BaseAction { +class PartnerAddAction extends ActionBuilder { const PartnerAddAction(); @override - IconData get icon => Icons.person_add_rounded; + ActionItem create(BuildContext context, WidgetRef ref) => + ActionItem(icon: Icons.person_add_rounded, label: context.t.add_partner, onAction: () => _add(context, ref)); - @override - String label(ActionScope scope) => scope.context.t.add_partner; + Future _add(BuildContext context, WidgetRef ref) async { + final partnerService = ref.read(partnerServiceProvider); + final authUserId = ref.read(authUserProvider).id; - @override - Future onAction(ActionScope scope) async { - final ActionScope(:context, :ref, :authUser) = scope; final selected = await showDialog(context: context, builder: (_) => const PartnerSelectionDialog()); if (selected == null) { return; } - await ref.read(partnerServiceProvider).create(sharedById: authUser.id, sharedWithId: selected.id); + try { + await partnerService.create(sharedById: authUserId, sharedWithId: selected.id); + } catch (error, stack) { + handleError(error, stack: stack, description: 'Failed to add partner'); + } } } -class PartnerRemoveAction extends BaseAction { +class PartnerRemoveAction extends ActionBuilder { const PartnerRemoveAction({required this.sharedWithId, required this.partnerName}); final String sharedWithId; final String partnerName; @override - IconData get icon => Icons.person_remove_rounded; + ActionItem create(BuildContext context, WidgetRef ref) => + ActionItem(icon: Icons.person_remove_rounded, label: context.t.remove, onAction: () => _remove(context, ref)); - @override - String label(ActionScope scope) => scope.context.t.remove; - - @override - Future onAction(ActionScope scope) async { - final ActionScope(:context, :ref, :authUser) = scope; + Future _remove(BuildContext context, WidgetRef ref) async { + final partnerService = ref.read(partnerServiceProvider); + final authUserId = ref.read(authUserProvider).id; final confirmed = await showDialog( context: context, @@ -56,19 +58,18 @@ class PartnerRemoveAction extends BaseAction { return; } - await ref.read(partnerServiceProvider).delete(sharedById: authUser.id, sharedWithId: sharedWithId); + try { + await partnerService.delete(sharedById: authUserId, sharedWithId: sharedWithId); + } catch (error, stack) { + handleError(error, stack: stack, description: 'Failed to remove partner'); + } } } @visibleForTesting -final candidatesStateProvider = StreamProvider.autoDispose>((ref) { - final currentUser = ref.watch(currentUserProvider); - // TODO: Refactor with a route guard to avoid this check in every provider - if (currentUser == null) { - return const Stream.empty(); - } - return ref.watch(partnerServiceProvider).getCandidates(currentUser.id); -}); +final candidatesStateProvider = StreamProvider.autoDispose>( + (ref) => ref.watch(partnerServiceProvider).getCandidates(ref.watch(authUserProvider).id), +); @visibleForTesting class PartnerSelectionDialog extends ConsumerWidget { diff --git a/mobile/lib/presentation/actions/timeline.action.dart b/mobile/lib/presentation/actions/timeline.action.dart deleted file mode 100644 index d8d367f674..0000000000 --- a/mobile/lib/presentation/actions/timeline.action.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:immich_mobile/presentation/actions/action.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; - -class TimelineAction extends BaseAction { - final BaseAction action; - - const TimelineAction({required this.action}); - - @override - IconData get icon => action.icon; - - @override - String label(ActionScope scope) => action.label(scope); - - @override - bool isVisible(ActionScope scope) => action.isVisible(scope); - - @override - Future onAction(ActionScope scope) async { - await action.onAction(scope); - scope.ref.read(multiSelectProvider.notifier).reset(); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart deleted file mode 100644 index 0f3ce47122..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/favorite_action_button.widget.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; -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/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_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 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class FavoriteActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const FavoriteActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - Future _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final result = await ref.read(actionProvider.notifier).favorite(source); - - if (source == ActionSource.viewer) { - if (result.success) { - final currentAsset = ref.read(assetViewerProvider).currentAsset; - if (currentAsset is RemoteAsset && !currentAsset.isFavorite) { - ref.read(assetViewerProvider.notifier).setAsset(currentAsset.copyWith(isFavorite: true)); - } - } - return; - } - - ref.read(multiSelectProvider.notifier).reset(); - if (!context.mounted) { - return; - } - - final successMessage = 'favorite_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.favorite_border_rounded, - label: "favorite".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart deleted file mode 100644 index be6c3b0180..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; -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/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_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 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class UnFavoriteActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const UnFavoriteActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - Future _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final result = await ref.read(actionProvider.notifier).unFavorite(source); - - if (source == ActionSource.viewer) { - if (result.success) { - final currentAsset = ref.read(assetViewerProvider).currentAsset; - if (currentAsset is RemoteAsset && currentAsset.isFavorite) { - ref.read(assetViewerProvider.notifier).setAsset(currentAsset.copyWith(isFavorite: false)); - } - } - return; - } - - ref.read(multiSelectProvider.notifier).reset(); - if (!context.mounted) { - return; - } - - final successMessage = 'unfavorite_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.favorite_rounded, - label: "unfavorite".t(context: context), - onPressed: () => _onTap(context, ref), - iconOnly: iconOnly, - menuItem: menuItem, - ); - } -} diff --git a/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart index 9c9f8f7139..deae9cbd07 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart @@ -46,7 +46,6 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0); final originalTheme = context.themeData; - final assetForAction = [asset]; final actions = [ if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true), @@ -66,7 +65,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { }, ), - ActionIconButtonWidget(action: FavoriteAction(assets: assetForAction)), + const ActionIconButton(action: FavoriteAction(source: .viewer)), ImmichColorOverride(color: null, child: ViewerKebabMenu(originalTheme: originalTheme)), ]; diff --git a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart index 3c9c0c692e..85fa8b4563 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart @@ -5,7 +5,6 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; -import 'package:immich_mobile/presentation/actions/timeline.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; @@ -76,9 +75,6 @@ class _ArchiveBottomSheetState extends ConsumerState { return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut); } - final assets = multiselect.selectedAssets.toList(growable: false); - final actions = [FavoriteAction(assets: assets)]; - return BaseBottomSheet( controller: sheetController, initialChildSize: 0.25, @@ -89,7 +85,7 @@ class _ArchiveBottomSheetState extends ConsumerState { if (multiselect.hasRemote) ...[ const ShareLinkActionButton(source: ActionSource.timeline), const UnArchiveActionButton(source: ActionSource.timeline), - ...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))), + const ActionColumnButton(action: FavoriteAction(source: .timeline)), if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline), isTrashEnable ? const TrashActionButton(source: ActionSource.timeline) diff --git a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart index 4382eeba5d..6447c5ccf8 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart @@ -6,7 +6,6 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; -import 'package:immich_mobile/presentation/actions/timeline.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; @@ -77,9 +76,6 @@ class FavoriteBottomSheet extends ConsumerWidget { ref.read(multiSelectProvider.notifier).reset(); } - final assets = multiselect.selectedAssets.toList(growable: false); - final actions = [FavoriteAction(assets: assets)]; - return BaseBottomSheet( initialChildSize: 0.4, maxChildSize: 0.7, @@ -88,7 +84,7 @@ class FavoriteBottomSheet extends ConsumerWidget { const ShareActionButton(source: ActionSource.timeline), if (multiselect.hasRemote) ...[ const ShareLinkActionButton(source: ActionSource.timeline), - ...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))), + const ActionColumnButton(action: FavoriteAction(source: .timeline)), const ArchiveActionButton(source: ActionSource.timeline), if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline), isTrashEnable diff --git a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart index 1949a79495..2f1ffe4cbc 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart @@ -4,8 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; -import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; -import 'package:immich_mobile/presentation/actions/timeline.action.dart'; +import 'package:immich_mobile/presentation/actions/favorite.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; @@ -14,7 +13,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permane import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; @@ -83,9 +81,6 @@ class _GeneralBottomSheetState extends ConsumerState { return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut); } - final assets = multiselect.selectedAssets.toList(growable: false); - final actions = [AssetDebugAction(assets: assets)]; - return BaseBottomSheet( controller: sheetController, initialChildSize: widget.minChildSize ?? 0.15, @@ -93,7 +88,7 @@ class _GeneralBottomSheetState extends ConsumerState { maxChildSize: 0.85, shouldCloseOnMinExtent: false, actions: [ - ...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))), + const ActionColumnButton(action: FavoriteAction(source: .timeline)), const ShareActionButton(source: ActionSource.timeline), if (multiselect.hasRemote) ...[ const ShareLinkActionButton(source: ActionSource.timeline), @@ -101,7 +96,7 @@ class _GeneralBottomSheetState extends ConsumerState { isTrashEnable ? const TrashActionButton(source: ActionSource.timeline) : const DeletePermanentActionButton(source: ActionSource.timeline), - const FavoriteActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: FavoriteAction(source: .timeline)), const ArchiveActionButton(source: ActionSource.timeline), if (tagsEnabled) const BulkTagAssetsActionButton(source: ActionSource.timeline), const EditDateTimeActionButton(source: ActionSource.timeline), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart index a292c1899c..f6cbc5eac9 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart @@ -5,7 +5,6 @@ import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; -import 'package:immich_mobile/presentation/actions/timeline.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; @@ -85,9 +84,6 @@ class _RemoteAlbumBottomSheetState extends ConsumerState return sheetController.animateTo(0.85, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut); } - final assets = multiselect.selectedAssets.toList(growable: false); - final actions = [FavoriteAction(assets: assets)]; - return BaseBottomSheet( controller: sheetController, initialChildSize: 0.22, @@ -101,7 +97,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState if (ownsAlbum) ...[ const ArchiveActionButton(source: ActionSource.timeline), - ...actions.map((action) => ActionColumnButtonWidget(action: TimelineAction(action: action))), + const ActionColumnButton(action: FavoriteAction(source: .timeline)), ], const DownloadActionButton(source: ActionSource.timeline), if (ownsAlbum) ...[ diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index 7e08686078..d4cd39bbd3 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -137,28 +137,6 @@ class ActionNotifier extends Notifier { } } - Future favorite(ActionSource source) async { - final ids = _getOwnedRemoteIdsForSource(source); - try { - await _service.favorite(ids); - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to favorite assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - - Future unFavorite(ActionSource source) async { - final ids = _getOwnedRemoteIdsForSource(source); - try { - await _service.unFavorite(ids); - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to unfavorite assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - Future archive(ActionSource source) async { final ids = _getOwnedRemoteIdsForSource(source); try { diff --git a/mobile/lib/providers/infrastructure/toast.provider.dart b/mobile/lib/providers/infrastructure/toast.provider.dart index 27d1cf9e6b..eaaffd6fca 100644 --- a/mobile/lib/providers/infrastructure/toast.provider.dart +++ b/mobile/lib/providers/infrastructure/toast.provider.dart @@ -1,4 +1,4 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/repositories/toast.repository.dart'; +import 'package:immich_mobile/services/toast.service.dart'; -final toastRepositoryProvider = Provider((ref) => const .new()); +final toastServiceProvider = Provider((ref) => const .new()); diff --git a/mobile/lib/providers/user.provider.dart b/mobile/lib/providers/user.provider.dart index 2feb39ce5c..427a1bf1c7 100644 --- a/mobile/lib/providers/user.provider.dart +++ b/mobile/lib/providers/user.provider.dart @@ -30,3 +30,11 @@ class CurrentUserProvider extends StateNotifier { final currentUserProvider = StateNotifierProvider((ref) { return CurrentUserProvider(ref.watch(userServiceProvider)); }); + +final authUserProvider = Provider((ref) { + final user = ref.watch(currentUserProvider); + if (user == null) { + throw Exception('User must be logged in to access this provider'); + } + return user; +}); diff --git a/mobile/lib/repositories/asset_api.repository.dart b/mobile/lib/repositories/asset_api.repository.dart index 2024b75c6e..9b92b491bf 100644 --- a/mobile/lib/repositories/asset_api.repository.dart +++ b/mobile/lib/repositories/asset_api.repository.dart @@ -111,11 +111,6 @@ class AssetApiRepository extends ApiRepository { ); } - // TODO(shenlong): remove after action migration - Future updateFavorite(List ids, bool isFavorite) async { - return _api.updateAssets(AssetBulkUpdateDto(ids: ids, isFavorite: Optional.present(isFavorite))); - } - Future updateLocation(List ids, LatLng location) async { return _api.updateAssets( AssetBulkUpdateDto( diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index 5986f0407d..dd1b3e8496 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -68,16 +68,6 @@ class ActionService { unawaited(context.pushRoute(SharedLinkEditRoute(assetsList: remoteIds))); } - Future favorite(List remoteIds) async { - await _assetApiRepository.updateFavorite(remoteIds, true); - await _remoteAssetRepository.updateFavorite(remoteIds, true); - } - - Future unFavorite(List remoteIds) async { - await _assetApiRepository.updateFavorite(remoteIds, false); - await _remoteAssetRepository.updateFavorite(remoteIds, false); - } - Future archive(List remoteIds) async { await _assetApiRepository.updateVisibility(remoteIds, .archive); await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.archive); diff --git a/mobile/lib/repositories/toast.repository.dart b/mobile/lib/services/toast.service.dart similarity index 91% rename from mobile/lib/repositories/toast.repository.dart rename to mobile/lib/services/toast.service.dart index 0cca50fdec..2b61a945ff 100644 --- a/mobile/lib/repositories/toast.repository.dart +++ b/mobile/lib/services/toast.service.dart @@ -9,8 +9,8 @@ class ToastOption { const ToastOption({this.timeout, this.onUndo}); } -class ToastRepository { - const ToastRepository(); +class ToastService { + const ToastService(); FutureOr success(String message, {ToastOption? toast}) { snackbar.success(message, duration: toast?.timeout); diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 0e5a3123e7..4219e0aed7 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -192,7 +192,7 @@ enum ActionButtonType { bool menuItem = false, ]) { return switch (this) { - ActionButtonType.advancedInfo => ActionMenuItemWidget(action: AssetDebugAction(assets: [context.asset])), + ActionButtonType.advancedInfo => ActionMenuItem(action: AssetDebugAction(source: context.source)), ActionButtonType.share => ShareActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), ActionButtonType.shareLink => ShareLinkActionButton( source: context.source, diff --git a/mobile/test/repository.mocks.dart b/mobile/test/repository.mocks.dart index 82c9395b58..b56a8a098a 100644 --- a/mobile/test/repository.mocks.dart +++ b/mobile/test/repository.mocks.dart @@ -6,7 +6,6 @@ import 'package:immich_mobile/repositories/auth.repository.dart'; import 'package:immich_mobile/repositories/auth_api.repository.dart'; import 'package:immich_mobile/repositories/download.repository.dart'; import 'package:immich_mobile/repositories/permission.repository.dart'; -import 'package:immich_mobile/repositories/toast.repository.dart'; import 'package:mocktail/mocktail.dart'; class MockAssetApiRepository extends Mock implements AssetApiRepository {} @@ -24,5 +23,3 @@ class MockTagService extends Mock implements TagService {} class MockDownloadRepository extends Mock implements DownloadRepository {} class MockRemoteExifRepository extends Mock implements RemoteExifRepository {} - -class MockToastRepository extends Mock implements ToastRepository {} diff --git a/mobile/test/service.mocks.dart b/mobile/test/service.mocks.dart index 300c54dcbb..785567de56 100644 --- a/mobile/test/service.mocks.dart +++ b/mobile/test/service.mocks.dart @@ -12,6 +12,7 @@ import 'package:immich_mobile/services/foreground_upload.service.dart'; import 'package:immich_mobile/services/gcast.service.dart'; import 'package:immich_mobile/services/network.service.dart'; import 'package:immich_mobile/services/server_info.service.dart'; +import 'package:immich_mobile/services/toast.service.dart'; import 'package:mocktail/mocktail.dart'; class MockApiService extends Mock implements ApiService {} @@ -43,3 +44,5 @@ class MockServerInfoService extends Mock implements ServerInfoService {} class MockCleanupService extends Mock implements CleanupService {} class MockBackgroundSyncManager extends Mock implements BackgroundSyncManager {} + +class MockToastService extends Mock implements ToastService {} diff --git a/mobile/test/unit/mocks.dart b/mobile/test/unit/mocks.dart index d8eadda7ae..7dd15eb9a3 100644 --- a/mobile/test/unit/mocks.dart +++ b/mobile/test/unit/mocks.dart @@ -30,7 +30,6 @@ class RepositoryMocks { final remoteAsset = RemoteAssetRepositoryStub(MockRemoteAssetRepository()); final remoteExif = RemoteExifRepositoryStub(MockRemoteExifRepository()); final trashedAsset = MockTrashedLocalAssetRepository(); - final toast = MockToastRepository(); final remoteAlbum = MockRemoteAlbumRepository(); final albumApi = MockDriftAlbumApiRepository(); @@ -56,7 +55,6 @@ class RepositoryMocks { assetApi.reset(); assetMedia.reset(); download.reset(); - reset(toast); _stubLocalAlbumRepository(); _stubLocalAssetRepository(); _stubRemoteAssetRepository(); @@ -115,6 +113,7 @@ class ServiceMocks { final upload = MockForegroundUploadService(); final cast = MockGCastService(); final serverInfo = MockServerInfoService(); + final toast = MockToastService(); ServiceMocks() { resetAll(); @@ -132,6 +131,7 @@ class ServiceMocks { reset(serverInfo); reset(backgroundSync); reset(upload); + reset(toast); _stubUserService(); _stubPartnerService(); _stubAssetService(); diff --git a/mobile/test/unit/presentation/actions/asset_debug_action_test.dart b/mobile/test/unit/presentation/actions/asset_debug_action_test.dart index 4e84c100cb..1644df0396 100644 --- a/mobile/test/unit/presentation/actions/asset_debug_action_test.dart +++ b/mobile/test/unit/presentation/actions/asset_debug_action_test.dart @@ -24,7 +24,8 @@ void main() { testWidgets('visible for a single asset when advanced troubleshooting is on', (tester) async { await tester.pumpTestWidget( context, - ActionIconButtonWidget(action: AssetDebugAction(assets: [RemoteAssetFactory.create()])), + const ActionIconButton(action: AssetDebugAction(source: .timeline)), + overrides: context.selected({RemoteAssetFactory.create()}), ); expect(find.byType(ImmichIconButton), findsOneWidget); @@ -33,9 +34,8 @@ void main() { testWidgets('hidden for multiple assets', (tester) async { await tester.pumpTestWidget( context, - ActionIconButtonWidget( - action: AssetDebugAction(assets: [RemoteAssetFactory.create(), RemoteAssetFactory.create()]), - ), + const ActionIconButton(action: AssetDebugAction(source: .timeline)), + overrides: context.selected({RemoteAssetFactory.create(), RemoteAssetFactory.create()}), ); expect(find.byType(ImmichIconButton), findsNothing); @@ -43,9 +43,11 @@ void main() { testWidgets('hidden when advanced troubleshooting is off', (tester) async { await StoreService.I.put(StoreKey.advancedTroubleshooting, false); + await tester.pumpTestWidget( context, - ActionIconButtonWidget(action: AssetDebugAction(assets: [RemoteAssetFactory.create()])), + const ActionIconButton(action: AssetDebugAction(source: .timeline)), + overrides: context.selected({RemoteAssetFactory.create()}), ); expect(find.byType(ImmichIconButton), findsNothing); diff --git a/mobile/test/unit/presentation/actions/favorite_action_test.dart b/mobile/test/unit/presentation/actions/favorite_action_test.dart index 722d9d1dc7..cb4d6130ab 100644 --- a/mobile/test/unit/presentation/actions/favorite_action_test.dart +++ b/mobile/test/unit/presentation/actions/favorite_action_test.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; +import 'package:immich_mobile/utils/option.dart'; +import 'package:immich_ui/immich_ui.dart'; import 'package:mocktail/mocktail.dart'; import '../../../service.mocks.dart'; @@ -24,55 +27,66 @@ void main() { RemoteAsset owned({bool isFavorite = false}) => RemoteAssetFactory.create(ownerId: context.currentUser.id, isFavorite: isFavorite); + Future pumpFavorite(WidgetTester tester, Set selection) => + tester.pumpTestAction(context, const FavoriteAction(source: .timeline), overrides: context.selected(selection)); + group('FavoriteAction', () { testWidgets('favorites the eligible owned assets', (tester) async { final asset = owned(); - await tester.pumpTestAction(context, FavoriteAction(assets: [asset])); + await pumpFavorite(tester, {asset}); - verify(() => assetService.updateFavorite([asset.id], true)).called(1); + verify(() => assetService.update([asset.id], isFavorite: const Option.some(true))).called(1); }); testWidgets('unfavorite the eligible owned assets', (tester) async { final asset = owned(isFavorite: true); - await tester.pumpTestAction(context, FavoriteAction(assets: [asset])); + await pumpFavorite(tester, {asset}); - verify(() => assetService.updateFavorite([asset.id], false)).called(1); + verify(() => assetService.update([asset.id], isFavorite: const Option.some(false))).called(1); }); testWidgets('ignores assets owned by someone else', (tester) async { final mine = owned(); final theirs = RemoteAssetFactory.create(); - await tester.pumpTestAction(context, FavoriteAction(assets: [mine, theirs])); + await pumpFavorite(tester, {mine, theirs}); - verify(() => assetService.updateFavorite([mine.id], true)).called(1); - }); - - testWidgets('batches every eligible owned asset into a single call', (tester) async { - final first = owned(); - final second = owned(); - - await tester.pumpTestAction(context, FavoriteAction(assets: [first, second])); - - verify(() => assetService.updateFavorite([first.id, second.id], true)).called(1); + verify(() => assetService.update([mine.id], isFavorite: const Option.some(true))).called(1); }); testWidgets('skips owned assets already in the target state', (tester) async { final stale = owned(); final alreadyFavorite = owned(isFavorite: true); - await tester.pumpTestAction(context, FavoriteAction(assets: [stale, alreadyFavorite])); + await pumpFavorite(tester, {stale, alreadyFavorite}); - verify(() => assetService.updateFavorite([stale.id], true)).called(1); + verify(() => assetService.update([stale.id], isFavorite: const Option.some(true))).called(1); }); testWidgets('shows a confirmation snackbar on success', (tester) async { - await tester.pumpTestAction(context, FavoriteAction(assets: [owned()])); + await pumpFavorite(tester, {owned()}); await tester.pumpUntilFound(find.byType(SnackBar)); expect(find.byType(SnackBar), findsOneWidget); }); + + testWidgets('clears the selection once the update succeeds', (tester) async { + await pumpFavorite(tester, {owned()}); + await tester.pumpAndSettle(); + + expect(find.byType(ImmichIconButton), findsNothing, reason: 'an empty selection hides the action'); + }); + + testWidgets('is hidden when none of the selected assets are owned', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: FavoriteAction(source: .timeline)), + overrides: context.selected({RemoteAssetFactory.create()}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); }); } diff --git a/mobile/test/unit/presentation/actions/timeline_action_test.dart b/mobile/test/unit/presentation/actions/timeline_action_test.dart deleted file mode 100644 index 5661be72c0..0000000000 --- a/mobile/test/unit/presentation/actions/timeline_action_test.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/presentation/actions/action.dart'; -import 'package:immich_mobile/presentation/actions/action.widget.dart'; -import 'package:immich_mobile/presentation/actions/timeline.action.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; - -import '../../factories/remote_asset_factory.dart'; -import '../presentation_context.dart'; - -class _FakeAction extends BaseAction { - _FakeAction({this.visible = true, this.error}); - - final bool visible; - final Object? error; - - bool ran = false; - bool? selectionDuringOnAction; - - @override - IconData get icon => Icons.bolt; - - @override - String label(ActionScope scope) => 'fake'; - - @override - bool isVisible(ActionScope scope) => visible; - - @override - Future onAction(ActionScope scope) async { - ran = true; - selectionDuringOnAction = scope.ref.read(multiSelectProvider).isEnabled; - if (error != null) { - throw error!; - } - } -} - -void main() { - late PresentationContext context; - - setUp(() async { - context = await PresentationContext.create(); - }); - - tearDown(() { - context.dispose(); - }); - - List overrides() => [ - multiSelectProvider.overrideWith( - () => MultiSelectNotifier( - MultiSelectState(selectedAssets: {RemoteAssetFactory.create()}, lockedSelectionAssets: const {}), - ), - ), - ]; - - Future<(ActionScope, ProviderContainer)> pumpScope(WidgetTester tester) async { - late ActionScope scope; - late ProviderContainer container; - await tester.pumpTestWidget( - context, - Consumer( - builder: (innerContext, ref, _) { - scope = ActionScope(context: innerContext, ref: ref, authUser: context.currentUser); - container = ProviderScope.containerOf(innerContext, listen: false); - return const SizedBox.shrink(); - }, - ), - overrides: overrides(), - ); - return (scope, container); - } - - group('TimelineAction', () { - testWidgets('runs the wrapped action and then clears the selection', (tester) async { - final inner = _FakeAction(); - final (scope, container) = await pumpScope(tester); - await TimelineAction(action: inner).onAction(scope); - - expect(inner.ran, isTrue); - expect(inner.selectionDuringOnAction, isTrue, reason: 'reset must run after the inner action, not before'); - expect(container.read(multiSelectProvider).isEnabled, isFalse); - }); - - testWidgets('rethrows and keeps the selection when the wrapped action throws', (tester) async { - final error = Exception('boom'); - final inner = _FakeAction(error: error); - final (scope, container) = await pumpScope(tester); - - await expectLater(TimelineAction(action: inner).onAction(scope), throwsA(same(error))); - - expect(inner.ran, isTrue); - expect(container.read(multiSelectProvider).isEnabled, isTrue); - }); - - testWidgets('delegates visibility to the wrapped action', (tester) async { - await tester.pumpTestWidget( - context, - ActionIconButtonWidget(action: TimelineAction(action: _FakeAction(visible: false))), - ); - - expect(find.byType(ActionIconButtonWidget), findsOneWidget); - expect(find.byIcon(Icons.bolt), findsNothing); - }); - }); -} diff --git a/mobile/test/unit/presentation/partner_page_test.dart b/mobile/test/unit/presentation/partner_page_test.dart index ee9c6a3575..e6ff8fc83f 100644 --- a/mobile/test/unit/presentation/partner_page_test.dart +++ b/mobile/test/unit/presentation/partner_page_test.dart @@ -3,7 +3,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/pages/library/partner/partner.page.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/partner.action.dart'; +import 'package:immich_ui/immich_ui.dart'; import '../factories/partner_user_factory.dart'; import '../factories/user_factory.dart'; @@ -17,12 +19,10 @@ void main() { group('PartnerSharedByList', () { testWidgets('shows the empty-state add button when there are no partners', (tester) async { - const action = PartnerAddAction(); - await tester.pumpTestWidget(context, const PartnerSharedByList(partners: [])); expect(find.byType(ListView), findsNothing); - expect(find.widgetWithIcon(TextButton, action.icon), findsOneWidget); + expect(find.descendant(of: find.byType(ActionButton), matching: find.byType(ImmichTextButton)), findsOneWidget); }); testWidgets('renders a tile per partner with name and email', (tester) async { @@ -39,9 +39,11 @@ void main() { testWidgets('renders a remove action for each partner', (tester) async { final partner1 = PartnerFactory.create(inTimeline: true); final partner2 = PartnerFactory.create(); - const action = PartnerRemoveAction(sharedWithId: '', partnerName: ''); await tester.pumpTestWidget(context, PartnerSharedByList(partners: [partner1, partner2])); - expect(find.byIcon(action.icon), findsNWidgets(2)); + expect( + find.descendant(of: find.byType(ActionIconButton), matching: find.byType(ImmichIconButton)), + findsNWidgets(2), + ); }); }); diff --git a/mobile/test/unit/presentation/presentation_context.dart b/mobile/test/unit/presentation/presentation_context.dart index 25de583049..a45c1c14f9 100644 --- a/mobile/test/unit/presentation/presentation_context.dart +++ b/mobile/test/unit/presentation/presentation_context.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/locales.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; @@ -16,6 +17,7 @@ import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; +import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/gcast.service.dart'; import 'package:immich_mobile/services/server_info.service.dart'; @@ -52,6 +54,12 @@ class PresentationContext { inLockedViewProvider.overrideWithValue(false), ]; + List selected(Set assets) => [ + multiSelectProvider.overrideWith( + () => MultiSelectNotifier(MultiSelectState(selectedAssets: assets, lockedSelectionAssets: const {})), + ), + ]; + static Future create() async { TestUtils.init(); if (_db == null) { @@ -103,10 +111,10 @@ extension PumpPresentationWidget on WidgetTester { Future pumpTestAction( PresentationContext context, - BaseAction action, { + ActionBuilder action, { List overrides = const [], }) async { - await pumpTestWidget(context, ActionIconButtonWidget(action: action), overrides: overrides); + await pumpTestWidget(context, ActionIconButton(action: action), overrides: overrides); await tap(find.byType(ImmichIconButton)); await pump(); } From 8e0ad9b2c4b6579eb6082a6d65e23a715ec42bfe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 12:55:30 +0200 Subject: [PATCH 24/81] fix(deps): update typescript-projects (#30305) Co-authored-by: Daniel Dietzler --- docs/mise.toml | 2 +- mise.lock | 80 +- mise.toml | 2 +- package.json | 2 +- packages/e2e-auth-server/package.json | 2 +- packages/plugin-sdk/package.json | 2 +- packages/scripts/package.json | 2 +- pnpm-lock.yaml | 5102 ++++++++--------- server/package.json | 12 +- web/package.json | 2 +- web/src/lib/components/timeline/Month.svelte | 6 +- .../modals/PersonMergeSuggestionModal.svelte | 1 + 12 files changed, 2567 insertions(+), 2648 deletions(-) diff --git a/docs/mise.toml b/docs/mise.toml index 1236b21160..c4fe5f1b68 100644 --- a/docs/mise.toml +++ b/docs/mise.toml @@ -28,4 +28,4 @@ run = "prettier --write ." run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}" [tools] -wrangler = "4.111.0" +wrangler = "4.114.0" diff --git a/mise.lock b/mise.lock index 3b05f22889..a4b395500b 100644 --- a/mise.lock +++ b/mise.lock @@ -82,6 +82,15 @@ url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353224133" version = "7.1.3-6" backend = "github:jellyfin/jellyfin-ffmpeg" +[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.windows-x64"] +checksum = "sha256:7b7168149689610296f3a187c717056ce0786cc125a31caf28056737e9ba1cc1" +url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_win64-clang-gpl.zip" +url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409036094" + +[[tools."github:jellyfin/jellyfin-ffmpeg"]] +version = "7.1.3-6" +backend = "github:jellyfin/jellyfin-ffmpeg" + [tools."github:jellyfin/jellyfin-ffmpeg".options] asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz" @@ -111,23 +120,6 @@ url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets version = "7.1.3-6" backend = "github:jellyfin/jellyfin-ffmpeg" -[tools."github:jellyfin/jellyfin-ffmpeg".options] -asset_pattern = "jellyfin-ffmpeg_*_portable_linux64-gpl.tar.xz" - -[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64"] -checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2" -url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz" -url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879" - -[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64-musl"] -checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2" -url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz" -url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879" - -[[tools."github:jellyfin/jellyfin-ffmpeg"]] -version = "7.1.3-6" -backend = "github:jellyfin/jellyfin-ffmpeg" - [tools."github:jellyfin/jellyfin-ffmpeg".options] asset_pattern = "jellyfin-ffmpeg_*_portable_mac64-gpl.tar.xz" @@ -140,10 +132,18 @@ url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets version = "7.1.3-6" backend = "github:jellyfin/jellyfin-ffmpeg" -[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.windows-x64"] -checksum = "sha256:7b7168149689610296f3a187c717056ce0786cc125a31caf28056737e9ba1cc1" -url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_win64-clang-gpl.zip" -url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409036094" +[tools."github:jellyfin/jellyfin-ffmpeg".options] +asset_pattern = "jellyfin-ffmpeg_*_portable_linux64-gpl.tar.xz" + +[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64"] +checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2" +url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz" +url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879" + +[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64-musl"] +checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2" +url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz" +url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879" [[tools."github:webassembly/binaryen"]] version = "version_124" @@ -291,43 +291,43 @@ url = "https://github.com/opentofu/opentofu/releases/download/v1.12.5/tofu_1.12. url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/484602547" [[tools.pnpm]] -version = "11.13.1" +version = "11.17.0" backend = "aqua:pnpm/pnpm" [tools.pnpm."platforms.linux-arm64"] -checksum = "sha256:b52db99d215ed7dc9563aed815953c62a6c1ffd7cd75803d3a07ad7e4f246aed" -url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-linux-arm64.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563983" +checksum = "sha256:730d17de742a3efbb020ba91d7acfc0456c6ba6ad1cd8eb49f4c229fe9f504d3" +url = "https://github.com/pnpm/pnpm/releases/download/v11.17.0/pnpm-linux-arm64.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/487422123" provenance = "github-attestations" [tools.pnpm."platforms.linux-arm64-musl"] -checksum = "sha256:2cadd4fc815c591f498a0a84c9e74a836e3e8c1236275f6e1cfd355bae6ae957" -url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-linux-arm64-musl.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563985" +checksum = "sha256:cc072c0e7bdd290f52fb9afdcea778ede69f96f0f4dae63623163fc11d4beec3" +url = "https://github.com/pnpm/pnpm/releases/download/v11.17.0/pnpm-linux-arm64-musl.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/487422121" provenance = "github-attestations" [tools.pnpm."platforms.linux-x64"] -checksum = "sha256:bd6d4b0e14536207ad76bc838f5980cecd968da15f69aae0b207380cca3f2e98" -url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-linux-x64.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563986" +checksum = "sha256:bdb1db01bf0f757495405a59a09c5c287f315889dc98d3b14bc374b9fe43a0bf" +url = "https://github.com/pnpm/pnpm/releases/download/v11.17.0/pnpm-linux-x64.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/487422122" provenance = "github-attestations" [tools.pnpm."platforms.linux-x64-musl"] -checksum = "sha256:ba19690f4ed1b64f1203ade14e9216352b46232d5582468b26a0160e0c9618c5" -url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-linux-x64-musl.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563980" +checksum = "sha256:db2e4eeecab336bd41bbbc7a39626b166f1a15bb189efcf6a1dafdf159f23fe7" +url = "https://github.com/pnpm/pnpm/releases/download/v11.17.0/pnpm-linux-x64-musl.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/487422116" provenance = "github-attestations" [tools.pnpm."platforms.macos-arm64"] -checksum = "sha256:765c2bf04e8129cb58c0f946e324262e418370b35a203b50b1f06a0567ef8bc1" -url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-darwin-arm64.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563984" +checksum = "sha256:1e9a35d76f9d382af365d95c64f3abf7815e24350653f5bb1a37e4546265bc2b" +url = "https://github.com/pnpm/pnpm/releases/download/v11.17.0/pnpm-darwin-arm64.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/487422117" provenance = "github-attestations" [tools.pnpm."platforms.windows-x64"] -checksum = "sha256:d8bebbc71df2702961c1d34a5e61196bc0aa3bbde33c33253f6afa3dd4546a6d" -url = "https://github.com/pnpm/pnpm/releases/download/v11.13.1/pnpm-win32-x64.zip" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/478563981" +checksum = "sha256:3025c174a6dcffec14a071a5c854cb0c350a09a8c7c2857a75f75cc7b29b939c" +url = "https://github.com/pnpm/pnpm/releases/download/v11.17.0/pnpm-win32-x64.zip" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/487422118" provenance = "github-attestations" [[tools.terragrunt]] diff --git a/mise.toml b/mise.toml index d069dc1b99..f01c07f980 100644 --- a/mise.toml +++ b/mise.toml @@ -16,7 +16,7 @@ config_roots = [ [tools] node = "24.15.0" -pnpm = "11.13.1" +pnpm = "11.17.0" terragrunt = "1.1.1" opentofu = "1.12.5" "npm:@openapitools/openapi-generator-cli" = "2.40.1" diff --git a/package.json b/package.json index 817988a100..2464012cee 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "format": "prettier --cache --check i18n/", "format:fix": "prettier --cache --write --list-different i18n" }, - "packageManager": "pnpm@11.13.1", + "packageManager": "pnpm@11.17.0", "engines": { "pnpm": ">=10.0.0" }, diff --git a/packages/e2e-auth-server/package.json b/packages/e2e-auth-server/package.json index 5e88465de4..b872dfe36d 100644 --- a/packages/e2e-auth-server/package.json +++ b/packages/e2e-auth-server/package.json @@ -13,5 +13,5 @@ "oidc-provider": "^9.0.0", "tsx": "^4.20.6" }, - "packageManager": "pnpm@11.13.1" + "packageManager": "pnpm@11.17.0" } diff --git a/packages/plugin-sdk/package.json b/packages/plugin-sdk/package.json index 51fee7efda..8afa00cfb4 100644 --- a/packages/plugin-sdk/package.json +++ b/packages/plugin-sdk/package.json @@ -27,7 +27,7 @@ "keywords": [], "author": "", "license": "GNU Affero General Public License version 3", - "packageManager": "pnpm@11.13.1", + "packageManager": "pnpm@11.17.0", "devDependencies": { "@extism/js-pdk": "^1.1.1", "@immich/sdk": "workspace:*", diff --git a/packages/scripts/package.json b/packages/scripts/package.json index c2128db479..66327e6150 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -32,5 +32,5 @@ "vite": "^8.0.16", "vitest": "^4.1.8" }, - "packageManager": "pnpm@11.13.1" + "packageManager": "pnpm@11.17.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4d3e4246a..1f9120825d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,31 +19,31 @@ importers: devDependencies: prettier: specifier: ^3.8.3 - version: 3.9.5 + version: 3.9.6 prettier-plugin-sort-json: specifier: ^4.2.0 - version: 4.2.0(prettier@3.9.5) + version: 4.2.0(prettier@3.9.6) .github: devDependencies: prettier: specifier: ^3.7.4 - version: 3.9.5 + version: 3.9.6 docs: dependencies: '@docusaurus/core': specifier: ~3.10.0 - version: 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/preset-classic': specifier: ~3.10.0 - version: 3.10.2(@algolia/client-search@5.56.0)(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3) + version: 3.10.2(@algolia/client-search@5.56.0)(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/theme-common': specifier: ~3.10.0 - version: 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/theme-mermaid': specifier: ~3.10.0 - version: 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@mdi/js': specifier: ^7.3.67 version: 7.4.47 @@ -52,31 +52,31 @@ importers: version: 1.6.1 '@mdx-js/react': specifier: ^3.0.0 - version: 3.1.1(@types/react@19.2.17)(react@19.2.7) + version: 3.1.1(@types/react@19.2.17)(react@19.2.8) autoprefixer: specifier: ^10.4.17 - version: 10.5.4(postcss@8.5.19) + version: 10.5.4(postcss@8.5.25) docusaurus-lunr-search: specifier: ^3.3.2 - version: 3.6.0(@docusaurus/core@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 3.6.0(@docusaurus/core@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) lunr: specifier: ^2.3.9 version: 2.3.9 postcss: specifier: ^8.4.25 - version: 8.5.19 + version: 8.5.25 prism-react-renderer: specifier: ^2.3.1 - version: 2.4.1(react@19.2.7) + version: 2.4.1(react@19.2.8) raw-loader: specifier: ^4.0.2 - version: 4.0.2(webpack@5.108.4(postcss@8.5.19)) + version: 4.0.2(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) react: specifier: ^19.0.0 - version: 19.2.7 + version: 19.2.8 react-dom: specifier: ^19.0.0 - version: 19.2.7(react@19.2.7) + version: 19.2.8(react@19.2.8) tailwindcss: specifier: ^3.2.4 version: 3.4.19(tsx@4.23.1)(yaml@2.9.0) @@ -86,19 +86,19 @@ importers: devDependencies: '@docusaurus/module-type-aliases': specifier: ~3.10.0 - version: 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/tsconfig': specifier: ^3.10.0 version: 3.10.2 '@docusaurus/types': specifier: ^3.10.0 - version: 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@typescript/native': specifier: npm:typescript@^7.0.2 version: typescript@7.0.2 prettier: specifier: ^3.7.4 - version: 3.9.5 + version: 3.9.6 typescript: specifier: npm:@typescript/typescript6@^6.0.2 version: '@typescript/typescript6@6.0.2' @@ -107,7 +107,7 @@ importers: devDependencies: '@eslint/js': specifier: ^10.0.0 - version: 10.0.1(eslint@10.7.0(jiti@2.7.0)) + version: 10.0.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) '@faker-js/faker': specifier: ^10.1.0 version: 10.5.0 @@ -122,7 +122,7 @@ importers: version: link:../packages/sdk '@playwright/test': specifier: ^1.44.1 - version: 1.61.1 + version: 1.62.0 '@socket.io/component-emitter': specifier: ^3.1.2 version: 3.1.2 @@ -149,16 +149,16 @@ importers: version: 17.4.2 eslint: specifier: ^10.0.0 - version: 10.7.0(jiti@2.7.0) + version: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.7.0(jiti@2.7.0)) + version: 10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.7.0(jiti@2.7.0)))(eslint@10.7.0(jiti@2.7.0))(prettier@3.9.5) + version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)))(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(prettier@3.9.6) eslint-plugin-unicorn: specifier: ^72.0.0 - version: 72.0.0(eslint@10.7.0(jiti@2.7.0)) + version: 72.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) exiftool-vendored: specifier: ^35.0.0 version: 35.21.0 @@ -176,31 +176,31 @@ importers: version: 7.0.0 prettier: specifier: ^3.7.4 - version: 3.9.5 + version: 3.9.6 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(@typescript/typescript6@6.0.2)(prettier@3.9.5) + version: 4.3.0(@typescript/typescript6@6.0.2)(prettier@3.9.6) sharp: specifier: ^0.34.5 version: 0.34.5 socket.io-client: specifier: ^4.7.4 - version: 4.8.3 + version: 4.8.3(supports-color@8.1.1) supertest: specifier: ^7.0.0 - version: 7.2.2 + version: 7.2.2(supports-color@8.1.1) typescript: specifier: npm:@typescript/typescript6@^6.0.2 version: '@typescript/typescript6@6.0.2' typescript-eslint: specifier: ^8.28.0 - version: 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) + version: 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) utimes: specifier: ^5.2.1 - version: 5.2.1 + version: 5.2.1(supports-color@8.1.1) vitest: specifier: ^4.0.0 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6)(jsdom@26.1.0(canvas@3.2.3))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) packages/cli: dependencies: @@ -222,7 +222,7 @@ importers: devDependencies: '@eslint/js': specifier: ^10.0.0 - version: 10.0.1(eslint@10.7.0(jiti@2.7.0)) + version: 10.0.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) '@immich/sdk': specifier: workspace:* version: link:../sdk @@ -261,16 +261,16 @@ importers: version: 15.0.0 eslint: specifier: ^10.0.0 - version: 10.7.0(jiti@2.7.0) + version: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.7.0(jiti@2.7.0)) + version: 10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.7.0(jiti@2.7.0)))(eslint@10.7.0(jiti@2.7.0))(prettier@3.9.5) + version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)))(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(prettier@3.9.6) eslint-plugin-unicorn: specifier: ^72.0.0 - version: 72.0.0(eslint@10.7.0(jiti@2.7.0)) + version: 72.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) globals: specifier: ^17.0.0 version: 17.7.0 @@ -279,22 +279,22 @@ importers: version: 5.5.0 prettier: specifier: ^3.7.4 - version: 3.9.5 + version: 3.9.6 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(@typescript/typescript6@6.0.2)(prettier@3.9.5) + version: 4.3.0(@typescript/typescript6@6.0.2)(prettier@3.9.6) typescript: specifier: npm:@typescript/typescript6@^6.0.2 version: '@typescript/typescript6@6.0.2' typescript-eslint: specifier: ^8.58.0 - version: 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) + version: 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) vite: specifier: ^8.0.0 - version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) vitest: specifier: ^4.0.0 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6)(jsdom@26.1.0(canvas@3.2.3))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) vitest-fetch-mock: specifier: ^0.4.0 version: 0.4.5(vitest@4.1.10) @@ -309,10 +309,10 @@ importers: version: 9.5.0 jose: specifier: ^6.0.0 - version: 6.2.3 + version: 6.2.4 oidc-provider: specifier: ^9.0.0 - version: 9.9.1 + version: 9.10.0(supports-color@8.1.1) tsx: specifier: ^4.20.6 version: 4.23.1 @@ -377,10 +377,10 @@ importers: version: 7.7.1 vite: specifier: ^8.0.16 - version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) vitest: specifier: ^4.1.8 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6)(jsdom@26.1.0(canvas@3.2.3))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) packages/sdk: dependencies: @@ -408,70 +408,70 @@ importers: version: 0.5.2 '@nestjs/bullmq': specifier: ^11.0.1 - version: 11.0.4(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(bullmq@5.80.5) + version: 11.0.4(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(bullmq@5.81.2(supports-color@8.1.1)) '@nestjs/common': specifier: ^11.0.4 - version: 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) '@nestjs/core': specifier: ^11.0.4 - version: 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': specifier: ^11.0.4 - version: 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28) + version: 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(supports-color@8.1.1) '@nestjs/platform-socket.io': specifier: ^11.0.4 - version: 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.28)(rxjs@7.8.2) + version: 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/websockets@11.1.28)(rxjs@7.8.2)(supports-color@8.1.1) '@nestjs/schedule': specifier: ^6.0.0 - version: 6.1.3(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28) + version: 6.1.3(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28) '@nestjs/swagger': specifier: ^11.4.2 - version: 11.4.5(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@typescript/typescript6@6.0.2)(reflect-metadata@0.2.2) + version: 11.4.6(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(@typescript/typescript6@6.0.2)(reflect-metadata@0.2.2) '@nestjs/websockets': specifier: ^11.0.4 - version: 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@nestjs/platform-socket.io@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(@nestjs/platform-socket.io@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@opentelemetry/api': specifier: ^1.9.0 version: 1.9.1 '@opentelemetry/context-async-hooks': specifier: ^2.0.0 - version: 2.9.0(@opentelemetry/api@1.9.1) + version: 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/exporter-prometheus': - specifier: ^0.220.0 - version: 0.220.0(@opentelemetry/api@1.9.1) + specifier: ^0.221.0 + version: 0.221.0(@opentelemetry/api@1.9.1) '@opentelemetry/instrumentation-http': - specifier: ^0.220.0 - version: 0.220.0(@opentelemetry/api@1.9.1) + specifier: ^0.221.0 + version: 0.221.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) '@opentelemetry/instrumentation-ioredis': - specifier: ^0.68.0 - version: 0.68.0(@opentelemetry/api@1.9.1) + specifier: ^0.69.0 + version: 0.69.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) '@opentelemetry/instrumentation-nestjs-core': - specifier: ^0.66.0 - version: 0.66.0(@opentelemetry/api@1.9.1) + specifier: ^0.67.0 + version: 0.67.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) '@opentelemetry/instrumentation-pg': - specifier: ^0.72.0 - version: 0.72.0(@opentelemetry/api@1.9.1) + specifier: ^0.73.0 + version: 0.73.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) '@opentelemetry/resources': specifier: ^2.0.1 - version: 2.9.0(@opentelemetry/api@1.9.1) + version: 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-metrics': specifier: ^2.0.1 - version: 2.9.0(@opentelemetry/api@1.9.1) + version: 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-node': - specifier: ^0.220.0 - version: 0.220.0(@opentelemetry/api@1.9.1) + specifier: ^0.221.0 + version: 0.221.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) '@opentelemetry/semantic-conventions': specifier: ^1.34.0 version: 1.43.0 '@react-email/components': specifier: ^1.0.0 - version: 1.0.12(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 1.0.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@react-email/render': specifier: ^2.0.0 - version: 2.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 2.1.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@socket.io/redis-adapter': specifier: ^8.3.0 - version: 8.3.0(socket.io-adapter@2.5.7) + version: 8.3.0(socket.io-adapter@2.5.7(supports-color@8.1.1))(supports-color@8.1.1) archiver: specifier: ^7.0.0 version: 7.0.1 @@ -483,16 +483,16 @@ importers: version: 6.0.0 body-parser: specifier: ^2.2.0 - version: 2.3.0 + version: 2.3.0(supports-color@8.1.1) bullmq: specifier: ^5.51.0 - version: 5.80.5 + version: 5.81.2(supports-color@8.1.1) chokidar: specifier: ^4.0.3 version: 4.0.3 compression: specifier: ^1.8.0 - version: 1.8.1 + version: 1.8.1(supports-color@8.1.1) cookie: specifier: ^1.0.2 version: 1.1.1 @@ -507,7 +507,7 @@ importers: version: 35.21.0 express: specifier: ^5.1.0 - version: 5.2.1 + version: 5.2.1(supports-color@8.1.1) fast-glob: specifier: ^3.3.2 version: 3.3.3 @@ -531,10 +531,10 @@ importers: version: 7.14.0 ioredis: specifier: ^5.8.2 - version: 5.11.1 + version: 5.11.1(supports-color@8.1.1) jose: specifier: ^6.0.0 - version: 6.2.3 + version: 6.2.4 js-yaml: specifier: ^4.1.0 version: 4.3.0 @@ -561,19 +561,19 @@ importers: version: 2.2.0 nest-commander: specifier: ^3.16.0 - version: 3.20.1(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@types/inquirer@8.2.13)(@types/node@24.13.3)(@typescript/typescript6@6.0.2) + version: 3.20.1(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(@types/inquirer@8.2.13)(@types/node@24.13.3)(@typescript/typescript6@6.0.2) nestjs-cls: specifier: ^6.0.0 - version: 6.2.1(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 6.2.1(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) nestjs-kysely: specifier: 3.1.2 - version: 3.1.2(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(kysely@0.28.17)(reflect-metadata@0.2.2) + version: 3.1.2(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(kysely@0.28.17)(reflect-metadata@0.2.2) nestjs-otel: specifier: ^8.0.0 - version: 8.1.0(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(rxjs@7.8.2) + version: 8.1.0(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(rxjs@7.8.2) nestjs-zod: specifier: ^5.3.0 - version: 5.4.0(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.4.5(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@typescript/typescript6@6.0.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6) + version: 5.5.0(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/swagger@11.4.6(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(@typescript/typescript6@6.0.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6) nodemailer: specifier: ^9.0.0 version: 9.0.3 @@ -591,13 +591,13 @@ importers: version: 3.4.9 react: specifier: ^19.0.0 - version: 19.2.7 + version: 19.2.8 react-dom: specifier: ^19.0.0 - version: 19.2.7(react@19.2.7) + version: 19.2.8(react@19.2.8) react-email: specifier: ^5.0.0 - version: 5.2.11 + version: 5.2.11(supports-color@8.1.1) reflect-metadata: specifier: ^0.2.0 version: 0.2.2 @@ -618,7 +618,7 @@ importers: version: 3.0.2 socket.io: specifier: ^4.8.1 - version: 4.8.3 + version: 4.8.3(supports-color@8.1.1) tailwindcss-preset-email: specifier: ^1.4.0 version: 1.4.1(tailwindcss@3.4.19(tsx@4.23.1)(yaml@2.9.0)) @@ -643,19 +643,19 @@ importers: devDependencies: '@eslint/js': specifier: ^10.0.0 - version: 10.0.1(eslint@10.7.0(jiti@2.7.0)) + version: 10.0.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) '@nestjs/cli': specifier: ^11.0.2 - version: 11.0.24(@swc/core@1.15.43(@swc/helpers@0.5.23))(@types/node@24.13.3)(esbuild@0.28.1)(lightningcss@1.33.0)(prettier@3.9.5) + version: 11.0.24(@swc/core@1.15.46(@swc/helpers@0.5.23))(@types/node@24.13.3)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(prettier@3.9.6)(uglify-js@3.19.3) '@nestjs/schematics': specifier: ^11.0.0 - version: 11.1.0(@typescript/typescript6@6.0.2)(chokidar@4.0.3)(prettier@3.9.5) + version: 11.1.0(@typescript/typescript6@6.0.2)(chokidar@4.0.3)(prettier@3.9.6) '@nestjs/testing': specifier: ^11.0.4 - version: 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@nestjs/platform-express@11.1.28) + version: 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(@nestjs/platform-express@11.1.28) '@swc/core': specifier: ^1.4.14 - version: 1.15.43(@swc/helpers@0.5.23) + version: 1.15.46(@swc/helpers@0.5.23) '@types/archiver': specifier: ^7.0.0 version: 7.0.0 @@ -730,19 +730,19 @@ importers: version: typescript@7.0.2 '@vitest/coverage-v8': specifier: ^4.0.0 - version: 4.1.10(vitest@3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.10.6)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.33.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 4.1.10(vitest@3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) eslint: specifier: ^10.0.0 - version: 10.7.0(jiti@2.7.0) + version: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.7.0(jiti@2.7.0)) + version: 10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.7.0(jiti@2.7.0)))(eslint@10.7.0(jiti@2.7.0))(prettier@3.9.5) + version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)))(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(prettier@3.9.6) eslint-plugin-unicorn: specifier: ^72.0.0 - version: 72.0.0(eslint@10.7.0(jiti@2.7.0)) + version: 72.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) globals: specifier: ^17.0.0 version: 17.7.0 @@ -754,37 +754,37 @@ importers: version: 7.0.0 prettier: specifier: ^3.7.4 - version: 3.9.5 + version: 3.9.6 prettier-plugin-organize-imports: specifier: ^4.0.0 - version: 4.3.0(@typescript/typescript6@6.0.2)(prettier@3.9.5) + version: 4.3.0(@typescript/typescript6@6.0.2)(prettier@3.9.6) sql-formatter: specifier: ^15.0.0 version: 15.8.2 supertest: specifier: ^7.1.0 - version: 7.2.2 + version: 7.2.2(supports-color@8.1.1) tailwindcss: specifier: ^3.4.0 version: 3.4.19(tsx@4.23.1)(yaml@2.9.0) testcontainers: specifier: ^12.0.0 - version: 12.0.4 + version: 12.0.4(supports-color@8.1.1) typescript: specifier: npm:@typescript/typescript6@^6.0.2 version: '@typescript/typescript6@6.0.2' typescript-eslint: specifier: ^8.28.0 - version: 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) + version: 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) unplugin-swc: specifier: ^1.4.5 - version: 1.5.9(@swc/core@1.15.43(@swc/helpers@0.5.23))(rollup@4.62.0) + version: 1.5.9(@swc/core@1.15.46(@swc/helpers@0.5.23))(rollup@4.62.0) vite-tsconfig-paths: specifier: ^6.0.0 - version: 6.1.1(@typescript/typescript6@6.0.2)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 6.1.1(@typescript/typescript6@6.0.2)(supports-color@8.1.1)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) vitest: specifier: ^3.0.0 - version: 3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.10.6)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.33.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + version: 3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) web: dependencies: @@ -799,7 +799,7 @@ importers: version: link:../packages/sdk '@immich/ui': specifier: ^0.83.0 - version: 0.83.0(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + version: 0.83.0(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) '@mapbox/mapbox-gl-rtl-text': specifier: 0.4.0 version: 0.4.0 @@ -811,22 +811,22 @@ importers: version: 2.2.0 '@photo-sphere-viewer/core': specifier: ^5.14.0 - version: 5.14.3 + version: 5.15.0 '@photo-sphere-viewer/equirectangular-video-adapter': specifier: ^5.14.0 - version: 5.14.3(@photo-sphere-viewer/core@5.14.3)(@photo-sphere-viewer/video-plugin@5.14.3(@photo-sphere-viewer/core@5.14.3)) + version: 5.15.0(@photo-sphere-viewer/core@5.15.0)(@photo-sphere-viewer/video-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0)) '@photo-sphere-viewer/markers-plugin': specifier: ^5.14.0 - version: 5.14.3(@photo-sphere-viewer/core@5.14.3) + version: 5.15.0(@photo-sphere-viewer/core@5.15.0) '@photo-sphere-viewer/resolution-plugin': specifier: ^5.14.0 - version: 5.14.3(@photo-sphere-viewer/core@5.14.3)(@photo-sphere-viewer/settings-plugin@5.14.3(@photo-sphere-viewer/core@5.14.3)) + version: 5.15.0(@photo-sphere-viewer/core@5.15.0)(@photo-sphere-viewer/settings-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0)) '@photo-sphere-viewer/settings-plugin': specifier: ^5.14.0 - version: 5.14.3(@photo-sphere-viewer/core@5.14.3) + version: 5.15.0(@photo-sphere-viewer/core@5.15.0) '@photo-sphere-viewer/video-plugin': specifier: ^5.14.0 - version: 5.14.3(@photo-sphere-viewer/core@5.14.3) + version: 5.15.0(@photo-sphere-viewer/core@5.15.0) '@types/geojson': specifier: ^7946.0.16 version: 7946.0.16 @@ -835,13 +835,13 @@ importers: version: 0.42.0 '@zoom-image/svelte': specifier: ^0.3.0 - version: 0.3.9(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + version: 0.3.9(svelte@5.56.8(@typescript-eslint/types@8.65.0)) dom-to-image: specifier: ^2.6.0 version: 2.6.0 fabric: specifier: ^7.0.0 - version: 7.4.0 + version: 7.4.0(supports-color@8.1.1) geo-coordinates-parser: specifier: ^1.7.4 version: 1.7.4 @@ -853,7 +853,7 @@ importers: version: 4.7.9 happy-dom: specifier: ^20.0.0 - version: 20.10.6 + version: 20.11.1 hls-video-element: specifier: ^1.5.11 version: 1.5.11 @@ -877,7 +877,7 @@ importers: version: 5.24.0 media-chrome: specifier: ^4.19.0 - version: 4.19.2(react@19.2.7) + version: 4.19.2(react@19.2.8) pmtiles: specifier: ^4.3.0 version: 4.4.1 @@ -886,25 +886,25 @@ importers: version: 1.5.4 simple-icons: specifier: ^16.0.0 - version: 16.26.0 + version: 16.27.1 socket.io-client: specifier: ~4.8.0 - version: 4.8.3 + version: 4.8.3(supports-color@8.1.1) svelte-gestures: specifier: ^5.2.2 version: 5.2.2 svelte-i18n: specifier: ^4.0.1 - version: 4.0.1(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + version: 4.0.1(svelte@5.56.8(@typescript-eslint/types@8.65.0)) svelte-jsoneditor: specifier: ^3.10.0 - version: 3.12.0(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + version: 3.13.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)) svelte-maplibre: specifier: ^1.2.5 - version: 1.3.0(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + version: 1.3.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)) svelte-persisted-store: specifier: ^0.12.0 - version: 0.12.0(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + version: 0.12.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)) tabbable: specifier: ^6.2.0 version: 6.5.0 @@ -913,7 +913,7 @@ importers: version: 3.6.0 tailwind-variants: specifier: ^3.2.2 - version: 3.2.2(tailwind-merge@3.6.0)(tailwindcss@4.3.2) + version: 3.2.2(tailwind-merge@3.6.0)(tailwindcss@4.3.3) thumbhash: specifier: ^0.1.1 version: 0.1.1 @@ -926,43 +926,43 @@ importers: devDependencies: '@eslint/js': specifier: ^10.0.0 - version: 10.0.1(eslint@10.7.0(jiti@2.7.0)) + version: 10.0.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) '@faker-js/faker': specifier: ^10.0.0 version: 10.5.0 '@koddsson/eslint-plugin-tscompat': specifier: ^0.2.0 - version: 0.2.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) + version: 0.2.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) '@socket.io/component-emitter': specifier: ^3.1.0 version: 3.1.2 '@sveltejs/adapter-static': specifier: ^3.0.8 - version: 3.0.10(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))) + version: 3.0.10(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))) '@sveltejs/enhanced-img': specifier: ^0.11.0 - version: 0.11.0(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(rollup@4.62.0)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 0.11.0(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(rollup@4.62.0)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) '@sveltejs/kit': specifier: ^2.56.1 - version: 2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) '@sveltejs/vite-plugin-svelte': specifier: 7.2.0 - version: 7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) '@tailwindcss/vite': specifier: ^4.2.4 - version: 4.3.2(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 4.3.3(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) '@testing-library/jest-dom': specifier: ^6.4.2 version: 6.9.1 '@testing-library/svelte': specifier: ^5.2.8 - version: 5.4.2(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))(vitest@4.1.10) + version: 5.4.2(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))(vitest@4.1.10) '@testing-library/user-event': specifier: ^14.5.2 version: 14.6.1(@testing-library/dom@10.4.1) '@trivago/prettier-plugin-sort-imports': specifier: ^6.0.2 - version: 6.0.2(prettier-plugin-svelte@4.1.1(prettier@3.9.5)(svelte@5.56.5(@typescript-eslint/types@8.64.0)))(prettier@3.9.5)(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + version: 6.0.2(prettier-plugin-svelte@4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.65.0)))(prettier@3.9.6)(supports-color@8.1.1)(svelte@5.56.8(@typescript-eslint/types@8.65.0)) '@types/chromecast-caf-sender': specifier: ^1.0.11 version: 1.0.11 @@ -992,22 +992,22 @@ importers: version: 17.4.2 eslint: specifier: ^10.2.1 - version: 10.7.0(jiti@2.7.0) + version: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.7.0(jiti@2.7.0)) + version: 10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) eslint-plugin-better-tailwindcss: specifier: ^4.5.0 - version: 4.6.1(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0))(tailwindcss@4.3.2) + version: 4.7.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(tailwindcss@4.3.3) eslint-plugin-compat: specifier: ^7.0.0 - version: 7.0.2(eslint@10.7.0(jiti@2.7.0)) + version: 7.0.2(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) eslint-plugin-svelte: specifier: ^3.12.4 - version: 3.20.0(eslint@10.7.0(jiti@2.7.0))(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + version: 3.22.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) eslint-plugin-unicorn: specifier: ^72.0.0 - version: 72.0.0(eslint@10.7.0(jiti@2.7.0)) + version: 72.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) factory.ts: specifier: ^1.4.1 version: 1.4.2 @@ -1016,40 +1016,40 @@ importers: version: 17.7.0 prettier: specifier: ^3.7.4 - version: 3.9.5 + version: 3.9.6 prettier-plugin-sort-json: specifier: ^4.1.1 - version: 4.2.0(prettier@3.9.5) + version: 4.2.0(prettier@3.9.6) prettier-plugin-svelte: specifier: ^4.0.0 - version: 4.1.1(prettier@3.9.5)(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + version: 4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.65.0)) rollup-plugin-visualizer: specifier: ^7.0.0 version: 7.0.1(rolldown@1.1.5)(rollup@4.62.0) svelte: - specifier: 5.56.5 - version: 5.56.5(@typescript-eslint/types@8.64.0) + specifier: 5.56.8 + version: 5.56.8(@typescript-eslint/types@8.65.0) svelte-check: specifier: ^4.4.6 - version: 4.7.3(@typescript/typescript6@6.0.2)(picomatch@4.0.5)(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + version: 4.7.3(@typescript/typescript6@6.0.2)(picomatch@4.0.5)(svelte@5.56.8(@typescript-eslint/types@8.65.0)) svelte-eslint-parser: specifier: ^1.3.3 - version: 1.8.0(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + version: 1.8.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)) tailwindcss: specifier: ^4.2.4 - version: 4.3.2 + version: 4.3.3 typescript: specifier: npm:@typescript/typescript6@^6.0.2 version: '@typescript/typescript6@6.0.2' typescript-eslint: specifier: ^8.45.0 - version: 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) + version: 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) vite: specifier: ^8.0.0 - version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + version: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) vitest: specifier: ^4.0.0 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6)(jsdom@26.1.0(canvas@3.2.3))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) packages: @@ -1788,26 +1788,26 @@ packages: '@codemirror/autocomplete@6.20.3': resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} - '@codemirror/commands@6.10.3': - resolution: {integrity: sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==} + '@codemirror/commands@6.10.4': + resolution: {integrity: sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==} '@codemirror/lang-json@6.0.2': resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} - '@codemirror/language@6.12.3': - resolution: {integrity: sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==} + '@codemirror/language@6.12.4': + resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==} '@codemirror/lint@6.9.7': resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==} - '@codemirror/search@6.7.0': - resolution: {integrity: sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==} + '@codemirror/search@6.7.1': + resolution: {integrity: sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==} - '@codemirror/state@6.6.0': - resolution: {integrity: sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==} + '@codemirror/state@6.7.1': + resolution: {integrity: sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==} - '@codemirror/view@6.43.1': - resolution: {integrity: sha512-+BIjw/AG3tDQ4pJgTLPYdAW25eDE66YsvM4LKyVPgGzVgZ4a9Wj1SRX8kPVKgBDdPt8oHtZ15F0qx7p0oOHdHw==} + '@codemirror/view@6.43.7': + resolution: {integrity: sha512-FZsExxkoxnAN+d9TgqXLg5g4A1oQwzX9WlkOT5i2PKkcW7xx3Bmu0vs90g6fo9Mpdsb/l96dnAraQ8932aO4/g==} '@colors/colors@1.5.0': resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} @@ -2795,8 +2795,8 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.9.1': - resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 @@ -2809,16 +2809,16 @@ packages: resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/config-helpers@0.6.0': - resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + '@eslint/config-helpers@0.7.0': + resolution: {integrity: sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/core@1.2.1': resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/css-tree@4.0.4': - resolution: {integrity: sha512-nxMparyhqVWQvadx9x8dIfubfIPOE+X2b2waua8fzdnM9vdp9rgVtwEZlG0TmCwEUz/d/f40fzvO/eqBwdxz0A==} + '@eslint/css-tree@4.0.5': + resolution: {integrity: sha512-iPmijIAq4hlIJB86PYmY/fcZORHtjphSqICDbwuw32A/JmkhZQ/K/6TjHE03zqf3n5yABpVcbRAMG8Mi9ojy8g==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/js@10.0.1': @@ -2853,14 +2853,14 @@ packages: peerDependencies: commander: ^11.1.0 - '@floating-ui/core@1.7.5': - resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} - '@floating-ui/dom@1.7.6': - resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} - '@floating-ui/utils@0.2.11': - resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} '@formatjs/ecma402-abstract@2.3.6': resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==} @@ -2886,16 +2886,16 @@ packages: '@formatjs/intl-localematcher@0.6.2': resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==} - '@fortawesome/fontawesome-common-types@7.2.0': - resolution: {integrity: sha512-IpR0bER9FY25p+e7BmFH25MZKEwFHTfRAfhOyJubgiDnoJNsSvJ7nigLraHtp4VOG/cy8D7uiV0dLkHOne5Fhw==} + '@fortawesome/fontawesome-common-types@7.3.1': + resolution: {integrity: sha512-k0C0sdHmZtAo6dRDtd1Z/qcpyHbL0CKsjV8seMY/21xGhY5Wsv0XRmiI/xEEH4y2c9b1+jvgNs/3EqhV27yUEA==} engines: {node: '>=6'} - '@fortawesome/free-regular-svg-icons@7.2.0': - resolution: {integrity: sha512-iycmlN51EULlQ4D/UU9WZnHiN0CvjJ2TuuCrAh+1MVdzD+4ViKYH2deNAll4XAAYlZa8WAefHR5taSK8hYmSMw==} + '@fortawesome/free-regular-svg-icons@7.3.1': + resolution: {integrity: sha512-q1EsmL7Q8DDnkRBUjSvrxbq7c9oVwwVjCn/xa5apKmdp65YSgzUg2y0Ltnd5aDbT6GdAQQdXql5Ha90ArqIReQ==} engines: {node: '>=6'} - '@fortawesome/free-solid-svg-icons@7.2.0': - resolution: {integrity: sha512-YTVITFGN0/24PxzXrwqCgnyd7njDuzp5ZvaCx5nq/jg55kUYd94Nj8UTchBdBofi/L0nwRfjGOg0E41d2u9T1w==} + '@fortawesome/free-solid-svg-icons@7.3.1': + resolution: {integrity: sha512-v0BLa0eqg7ubvVWeNSHVBs8fWH/GJicERZoJaxJ3FE/lj67VSqzoMg9pzfZVOfLMX10y0pGwQAuxoRVVH2patg==} engines: {node: '>=6'} '@golevelup/nestjs-discovery@5.0.0': @@ -3517,8 +3517,8 @@ packages: '@maplibre/vt-pbf@4.3.2': resolution: {integrity: sha512-j6p0AdjvAR19Z3XaCysle7A4ZSo08tYOzxD0Y9NQylwPAkwJJeYub5b2eVucdeDh7erhv69DahoLOevDRERRUw==} - '@marijn/find-cluster-break@1.0.2': - resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + '@marijn/find-cluster-break@1.0.3': + resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==} '@mdi/js@7.4.47': resolution: {integrity: sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==} @@ -3684,10 +3684,10 @@ packages: prettier: optional: true - '@nestjs/swagger@11.4.5': - resolution: {integrity: sha512-lvndlJmWBVDOUT0uEtLi6sSpW1syK2/nbAlHBhiELBORMpJGe9+EiWAT9qHtB10jW91L2Jmlwkr0/lttsYZrig==} + '@nestjs/swagger@11.4.6': + resolution: {integrity: sha512-Le136h2WC7HGsd70+WyK1qrm+Zq7kFxBLkYC1JgAVqNRCt8kNh7bMF7Qkn65D5j2t/aks0+VbWmUVlYIwPrs3A==} peerDependencies: - '@fastify/static': ^8.0.0 || ^9.0.0 + '@fastify/static': ^8.0.0 || ^9.0.0 || ^10.0.0 '@nestjs/common': ^11.0.1 '@nestjs/core': ^11.0.1 class-transformer: '*' @@ -3754,94 +3754,94 @@ packages: '@oazapfts/runtime@1.2.0': resolution: {integrity: sha512-fi7dp7dNayyh/vzqhf0ZdoPfC7tJvYfjaE8MBL1yR+iIsH7cFoqHt+DV70VU49OMCqLc7wQa+yVJcSmIRnV4wA==} - '@opentelemetry/api-logs@0.220.0': - resolution: {integrity: sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==} + '@opentelemetry/api-logs@0.221.0': + resolution: {integrity: sha512-OlanaW1vv7ufTqQ3/fPLI4arGt5ZoM+P8abOMki6uEYnpRazepSWDwDnnw+la7kE26SHVC18//SMccrDvLKOXQ==} engines: {node: '>=8.0.0'} '@opentelemetry/api@1.9.1': resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} engines: {node: '>=8.0.0'} - '@opentelemetry/configuration@0.220.0': - resolution: {integrity: sha512-glfIVKnZevRin8fY/9uES/mhRtMT1lGINLHc9MIo5fTQZXswEEHamJtgjv4MTtzgnhHGC92mIS/0lzAUZMyE0w==} + '@opentelemetry/configuration@0.221.0': + resolution: {integrity: sha512-uE9y56Zdi9Gt/RdxYnVOo3YmFZkKJJMA0gqtBe8wh8gdtF5Asqe+Oh/TWiDtFb1s+31jNY4CWgnfIB1KOITfFA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.9.0 - '@opentelemetry/context-async-hooks@2.9.0': - resolution: {integrity: sha512-OQ0vzvbZBiUhjqLnUaoNfYmP8553Crr3aggB4y0ZUi815mZ7idpdJXQmoKdeBKJelYttoBlLSSHubmyw3wvX4w==} + '@opentelemetry/context-async-hooks@2.10.0': + resolution: {integrity: sha512-bvyMcgLEkozzSzpEEEo1OMoeQ97bxj6Qs2uN3mPrSdDvObMI1myffD/BPqcLlzZO9//d1SqQA/WPw7Cz2AiqhA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/core@2.9.0': - resolution: {integrity: sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==} + '@opentelemetry/core@2.10.0': + resolution: {integrity: sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/exporter-logs-otlp-grpc@0.220.0': - resolution: {integrity: sha512-s0sRPCSlXYqlgObOpCftomJllp3LfUL9FobQ5csg2172ydVhSEnu1ptpsVBJadazs5nUNp7vDuLE03FAFWTLOQ==} + '@opentelemetry/exporter-logs-otlp-grpc@0.221.0': + resolution: {integrity: sha512-txG1G0IrYSsKKMeiWZfj/i5cQmWB+h+hf3HzPpF3RqZVwp+iQQEIsv8Vtmzy6RWVdHdJZfygmVrBI39YTBvWcw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-http@0.220.0': - resolution: {integrity: sha512-8186thl+pTw64iz/qEEen5oJZoZ/gO73XruChdaGlYdWOdBIQ42r+vHLf6a7vIDqTD4b8ZOoMlyxptanECaI9A==} + '@opentelemetry/exporter-logs-otlp-http@0.221.0': + resolution: {integrity: sha512-nKXkr4Tomi6fjYVOf+ytcW3dZAVr4v4Bv5gsT6dr2gvpUPJpKgHB4XbMufMsPotRE3g0XH2GwVVCkN2w6SON+Q==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-logs-otlp-proto@0.220.0': - resolution: {integrity: sha512-8LZAxdJ0ENDAFwr4j0oY35mHBltiSzvlhdQAPGiC7p9VnxtuSq4SW1gfBAdW6t6hiQG6OwUl8w7KHaOdJPKHWg==} + '@opentelemetry/exporter-logs-otlp-proto@0.221.0': + resolution: {integrity: sha512-AH6EY+47gXFaWYgG3hfeOneGiE9xIZGtDBk+9g0sM8NZWzsQhhmqPbQQXJzS7pyCh5jRRr2nYNXVrkCmoojRvQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-grpc@0.220.0': - resolution: {integrity: sha512-U128izvJfX/dW9jRGP0gIfadR1Hg7ft3UEGIeRxLFK70m2BWw6AtNCOnsUygpw2zCgR/ygdWbGpcL6TmhW0ZGw==} + '@opentelemetry/exporter-metrics-otlp-grpc@0.221.0': + resolution: {integrity: sha512-KOgCtO15FC6C1T/xOqBcr7EyUs7B+7yomGNb5Y97d3s38rPbCCk5sewkmE2b0/itOkQ/PptX8CLlD+kn2mEtTg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-http@0.220.0': - resolution: {integrity: sha512-Yqt3RBw/bRVncaE9qIIhk4WfjbAQqXuP9FgAaU+IKPndnLEp/cUqZlSC324+bpmduRz7DoTjig8Ub0PeILWXUA==} + '@opentelemetry/exporter-metrics-otlp-http@0.221.0': + resolution: {integrity: sha512-sRfCKbOzgy8xZQV2as0RzIZlnCmCseCKZGLfRcrpo2CBngJDr+rPtX0zkG0+oUCV5kfQPUoW3W3C96Ag3Y/Clg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-metrics-otlp-proto@0.220.0': - resolution: {integrity: sha512-lyO+IQBdSvqHN/ZOW/OzrSWemtfD+HgWngn+HBNLhjy0YrCQQTz0OE/kSekH2Pl340dn9DWzhqHdz5Eftr+HLA==} + '@opentelemetry/exporter-metrics-otlp-proto@0.221.0': + resolution: {integrity: sha512-YMF4LveY2I3yhw61rn6nmC9FE8U24IZHPeKU1Duc5+sbwjMd8FwZAwba318ImdThCg/HuVQvhm2y6bfgNPnfYg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-prometheus@0.220.0': - resolution: {integrity: sha512-JZD5DL/NBpVd2BHefvYosm3G40UZ/KzExLv5tc0eZe0CtrsHHtcOk3YPUxR2EINmUeBf8+w5UReTV8fFPn95lA==} + '@opentelemetry/exporter-prometheus@0.221.0': + resolution: {integrity: sha512-kW79a20qWESIuAdDrxzg9WKM98twV/NBWBFRAH57ap/+ssZhiCo0hckzKT0zpuwR/gSHrFAQhJL0bYDrnEM34g==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-grpc@0.220.0': - resolution: {integrity: sha512-bv1xmNhmNwIM6MdUBw4yYuJeVcEViVLk3uD69vOQMwueHBnfyl/u0HnBlB1FNY/Te0UOzJzvcbyR8wN6b+iGbA==} + '@opentelemetry/exporter-trace-otlp-grpc@0.221.0': + resolution: {integrity: sha512-zXminlZedtq9LvOW64CnNkOqk15zV75k8JgtdTuWFge6+jk2m4GmAUm6L2eIiG1o2a2bZxXw2PDrszm+bps0IA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-http@0.220.0': - resolution: {integrity: sha512-/+ExB3lRkf+erv4PnoywyL7RHKITidxtUpUTS55k7OQ0dB42S7gEF1gry7swb9MSm1hYLUhJg4QQh9W8SpwwqA==} + '@opentelemetry/exporter-trace-otlp-http@0.221.0': + resolution: {integrity: sha512-AySXiKoC+meiWm6zdVj5T2LnPDZuatveBby1cMOeQteIWsYXAUxs8Sru13G2pVSPrUXz6vF+og7QVBX6GdC/oQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-trace-otlp-proto@0.220.0': - resolution: {integrity: sha512-voTAD8XgJxlK7zLkXh8EzMB09zrQr3tyY/BsnDTlDiQU/UdK58MZ63A3mUjdEDrxMjCVmBHU3WQJhRmQe+Dvzg==} + '@opentelemetry/exporter-trace-otlp-proto@0.221.0': + resolution: {integrity: sha512-Z9i2T7vgZbWe9rSLYxXVIbeW+XyzUq4rZanW3ZyVNwVDqCsh0EJKUgBWWQ0CZfeuUA+RQPzKgJQHMuWAUnKqXw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/exporter-zipkin@2.9.0': - resolution: {integrity: sha512-RwINoce2BH8T4obT5pMcAla2sWma1YZvYuaktWmTluQ0PkQdvv5D060rWI1+kawX+J2qBRcMbwrZJJNcMJUauQ==} + '@opentelemetry/exporter-zipkin@2.10.0': + resolution: {integrity: sha512-7gsvgf0UDoJ4l9ObrwBmz5G/ZogiPk+lq+g5GpLp24YQF/vPM/BSsnOfcLnfinast5ASUgLo78uSC/ObjlnXgg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.0.0 @@ -3852,62 +3852,62 @@ packages: peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-http@0.220.0': - resolution: {integrity: sha512-Szt4dO2Boz2CDr38DaSw/lnqwhwKl+IAdgNGEGgSm2Anb+fwPtIAGmIwkhsLLN69QQZQE96JxjMKYY4rlRkYKw==} + '@opentelemetry/instrumentation-http@0.221.0': + resolution: {integrity: sha512-oIP91CPIANuYr09tGFElPFKAh6JUar+awJf1kBRYlaeo9b0gDwZHEB2zBfFlvdNFHm0wAVutMZODVi5smKT30g==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-ioredis@0.68.0': - resolution: {integrity: sha512-M2MWPoKiMlNWzmW7+AwEwiFpTJQ7bhKpZuo9L3MS/z/KFm2yXY6B43IprAVyiszLFg8/JsF39t8b8wkGpxip2g==} + '@opentelemetry/instrumentation-ioredis@0.69.0': + resolution: {integrity: sha512-I9sZtxXWZ1tRXtRNTEVxpokGtXy6RL1SZhtPVh7zxH78t8ar71V5Dx4bnQiUjKTDzItpC73krD8c0/cEWA9oLg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-nestjs-core@0.66.0': - resolution: {integrity: sha512-ZCzcTWXwlmQsLWGARbUz5fCLpYABoo5A/3PuV5+iICV3pmKWT0rRdKDevkRo0prbzJVh9oEyuT1idxI8ipDqXg==} + '@opentelemetry/instrumentation-nestjs-core@0.67.0': + resolution: {integrity: sha512-lXb7pjobd2i/9Gmihf9wrOM0MgnDYBxOJq7uWEhZOqULodNFLyPCRUnWxSIbPQdUzlU36QtHjuAeaEBUJnqXkA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation-pg@0.72.0': - resolution: {integrity: sha512-p9xrFc/6R8t6Y293sTYLZ83LnzZo/qY0bBPA4xabdQt0Qjt8i1SlYFsIeGY2Jmf5WcESNUdjQB3NxWnt5Ox7zw==} + '@opentelemetry/instrumentation-pg@0.73.0': + resolution: {integrity: sha512-yf3tBVwLHB9cZNNPSToNrthx36ouPe4FctFxy7ya6vSJ6gaiKjNfA/IgFFeuBpZflEQhy6aesPqzZo8ZjFkvNg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/instrumentation@0.220.0': - resolution: {integrity: sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==} + '@opentelemetry/instrumentation@0.221.0': + resolution: {integrity: sha512-cCk80Z/iRDf/5gfsKMB4f74LqVA5yKETB/9ojPzVW/6/f70iu89nJvGxsFCxx4XfSohaOofkU19kiYm84AiAlw==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-exporter-base@0.220.0': - resolution: {integrity: sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==} + '@opentelemetry/otlp-exporter-base@0.221.0': + resolution: {integrity: sha512-UFPIq80OH3Ns/oPFHRj14d4DTOxUo+MUFU8hUiCq5jTqFhdeJnfVSANHT+xp92409cA+oxzvlZCe6NM1wvCuBA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-grpc-exporter-base@0.220.0': - resolution: {integrity: sha512-/eIkBPMBTIvM3x/0mDX4aJeSkYifYClnBPr68PL1h5LV4VQv4+SV6CGrpiZ4fIWDnobVmhTWCm1J/QRdAWUfvA==} + '@opentelemetry/otlp-grpc-exporter-base@0.221.0': + resolution: {integrity: sha512-rQDmNgyiGCTrescjnzH2ntVyUKVIq6I2UjuK8+stT/Xg0ZOT71FVJqwjFdspQl6Yol/Yqsut9bDo+ame8oTmDQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/otlp-transformer@0.220.0': - resolution: {integrity: sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==} + '@opentelemetry/otlp-transformer@0.221.0': + resolution: {integrity: sha512-lg6lkOU08Az23jVcn/0Els9HP+V8PnR4Km6p0KgpTggS0n/WuhnmY64rSh83Of9iR9nD+dpWr6adlcX8KzAwjg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': ^1.3.0 - '@opentelemetry/propagator-b3@2.9.0': - resolution: {integrity: sha512-WrOT1WsOUG+B7hstD2RYoMPIOK76G8E9AQHhMjUvrQaGx/oA7rPWQvvr1Rqv7+yy4R0ZMVwWLC4vW2xnkgWPAQ==} + '@opentelemetry/propagator-b3@2.10.0': + resolution: {integrity: sha512-GnA5B24H+1w8BO21J0q+IWNB0z1v+AGbcquTdIt/dufibhnhgxaA8YKvz0I3akRZhB1jHT+/tlzK+qlAjEDybQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/propagator-jaeger@2.9.0': - resolution: {integrity: sha512-4mYGty27rYvSM0jtp1ZUOqd3LfVRCYg9H5G9OFzSx5HViYToU21MFhWfco7x1HwXr7ER8yGOiCIHZUwjPksc0Q==} + '@opentelemetry/propagator-jaeger@2.10.0': + resolution: {integrity: sha512-yw/IX8DL470dSMZJoE82ScfYGp7JWZ/G8kFJo35ZILUVTB2jFPTOaioN+8s09pH0RHsWNhweVZb+ZnjJJpCChg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' @@ -3916,44 +3916,44 @@ packages: resolution: {integrity: sha512-VCghU1JYs/4gP6Gqf/xro9MEsZ7LrMv2uONVsaESKL38ZOB9BqnI98FfS23wjMnHlpuE+TTaWSoAVNpTwYXzjw==} engines: {node: ^18.19.0 || >=20.6.0} - '@opentelemetry/resources@2.9.0': - resolution: {integrity: sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==} + '@opentelemetry/resources@2.10.0': + resolution: {integrity: sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-logs@0.220.0': - resolution: {integrity: sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==} + '@opentelemetry/sdk-logs@0.221.0': + resolution: {integrity: sha512-FaDcazjyMp7TZZZAsqbo4IkovP0UegoCu0EBkiNt+qCqvUf7FPAsfcrZ3+ZEkKgXZ/jHafop+JoGPDk3A0SmLg==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.4.0 <1.10.0' - '@opentelemetry/sdk-metrics@2.9.0': - resolution: {integrity: sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==} + '@opentelemetry/sdk-metrics@2.10.0': + resolution: {integrity: sha512-t6r1VSvXNtSDnPXU1FbZeetJb7yyovHmgu0wRSoftxtE0g2rSNhQZQUy69sRUCL+iioJpX8SN/S6wq6ZtvLySQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.9.0 <1.10.0' - '@opentelemetry/sdk-node@0.220.0': - resolution: {integrity: sha512-wHtGyHhSKHNH3fym33xRu4Ef/HXTFvX8eQ42xdQdEO9LYx9Y2qNyBDJytyqVlvmo6abWZlNYTUthuAGUMYqYnQ==} + '@opentelemetry/sdk-node@0.221.0': + resolution: {integrity: sha512-UbYuvtBrQQB5Prsh9KOKy4kxzexFxfMs5MkteHeWMoswsEB7kiNhyUVkAOFW/qsEzNHtrkgyghrD2ilZJa+5YA==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace-base@2.9.0': - resolution: {integrity: sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag==} + '@opentelemetry/sdk-trace-base@2.10.0': + resolution: {integrity: sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/sdk-trace-node@2.9.0': - resolution: {integrity: sha512-ec9a7ps37huy5itYk0MalaZdSLlM6AXWp/FhtEjgMpp5leEGojBDvAl/UWttQnkMZOvFHKzRESn8TD3yKTF5nQ==} + '@opentelemetry/sdk-trace-node@2.10.0': + resolution: {integrity: sha512-GZK/G6oZyBLGlH1pUgeDch7D91KoHd2uotUGIkWCPi9GI5T9X0p4L7nNAMDR1BQjkRYoDqo+ddfVx9t5Uhys+Q==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/sdk-trace@2.9.0': - resolution: {integrity: sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==} + '@opentelemetry/sdk-trace@2.10.0': + resolution: {integrity: sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==} engines: {node: ^18.19.0 || >=20.6.0} peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' @@ -4099,35 +4099,35 @@ packages: resolution: {integrity: sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==} engines: {node: '>=20.0.0'} - '@photo-sphere-viewer/core@5.14.3': - resolution: {integrity: sha512-gSDzM1fuVfWQiDf8Y4AgI+un/wsKmeFO7cY4IFEbjhD1BTXUviSL8GuCXABNfPK2EThZLJqpOXxrbYAhblJrfQ==} + '@photo-sphere-viewer/core@5.15.0': + resolution: {integrity: sha512-Mf72l3R8iBf+Im5duv7IVkX3uiKU1BT6bzzXmdE43KahQgNftt9pYKqTzN1YjaYLAzWDBCFZj/bEK1dIZXk9Gg==} - '@photo-sphere-viewer/equirectangular-video-adapter@5.14.3': - resolution: {integrity: sha512-jgdFMAGkVgtybq0d+zP3Qbu8SNw1Makw/EOpqyeo1klHK+S5JBRp1ibtq/9NRPtYTlf3Ral03iYR6iDgJuxJbg==} + '@photo-sphere-viewer/equirectangular-video-adapter@5.15.0': + resolution: {integrity: sha512-Gro1wUA5qTla71uSeGlQFr9BenNXrI9Xee4a+dveafqrZJ35/n+nDhxrc8I4osykyvC6Foc0qgksAsaNsMKX/w==} peerDependencies: - '@photo-sphere-viewer/core': 5.14.3 - '@photo-sphere-viewer/video-plugin': 5.14.3 + '@photo-sphere-viewer/core': 5.15.0 + '@photo-sphere-viewer/video-plugin': 5.15.0 - '@photo-sphere-viewer/markers-plugin@5.14.3': - resolution: {integrity: sha512-OAXHfXR2riaBwS8+OhqcAT2KZ3ch1QI9mJaBq6R9Abs+hcZ5f6QbWJ0IS9SFIGcdHFgBbz6+ea1FcwA9V3zx4g==} + '@photo-sphere-viewer/markers-plugin@5.15.0': + resolution: {integrity: sha512-r1Kv4BkvcOf+77QU7k0pHzZ3O0IrX+ff7arttmb0D81HQHRl/fUcE+tXu+mGDj9B1zuSi1LcvKOsUI9m96owNw==} peerDependencies: - '@photo-sphere-viewer/core': 5.14.3 + '@photo-sphere-viewer/core': 5.15.0 - '@photo-sphere-viewer/resolution-plugin@5.14.3': - resolution: {integrity: sha512-3W4DMhWcYjZdc65NjjLk49Nr7FTczCeRu6DVApfaN/OXfKW6IEFlWKJyrLpXUdQaNB8LPk6GuD0MFNGp8SHAwQ==} + '@photo-sphere-viewer/resolution-plugin@5.15.0': + resolution: {integrity: sha512-8Rm9r9WJX5m8N0Q7ezUEHUgxWybroj6HhfaksINUm7o8WegqZ+Pp2izEEoXI57kkjfudYOp7abrwA/J3I+QRrA==} peerDependencies: - '@photo-sphere-viewer/core': 5.14.3 - '@photo-sphere-viewer/settings-plugin': 5.14.3 + '@photo-sphere-viewer/core': 5.15.0 + '@photo-sphere-viewer/settings-plugin': 5.15.0 - '@photo-sphere-viewer/settings-plugin@5.14.3': - resolution: {integrity: sha512-EMaBRXIBIfP+jTOQSvkN00Bhh+ia+G8Bcxak0ydoGb5yaDg9w86Q7/NdK0NNpwXFqcQIAaoDzyX6UEi8Udic5g==} + '@photo-sphere-viewer/settings-plugin@5.15.0': + resolution: {integrity: sha512-xZot+fT5R+Jy7D/QEVoylIMg/R+l/iSBykQ1v8wqGkSrUqJdVkNv3w3ksycb9BYFt/78P8HU7AGvJgIF6J3EFQ==} peerDependencies: - '@photo-sphere-viewer/core': 5.14.3 + '@photo-sphere-viewer/core': 5.15.0 - '@photo-sphere-viewer/video-plugin@5.14.3': - resolution: {integrity: sha512-yMUd3JG0ROJpJkPXJbdFiIkz+m6lDMRrIeLm/bwXvaX+Q1uSgyX0ipIkJrQnAtdk/IU5fdyqBwfn9ExlQ27c2A==} + '@photo-sphere-viewer/video-plugin@5.15.0': + resolution: {integrity: sha512-RG70vRu+suV7QT1WbnxVknm8KVU26wCZx4NUofFAENYSvyldlRxmVK3XqCh1WChWhcjrXqj3woz4vnzrykVOzQ==} peerDependencies: - '@photo-sphere-viewer/core': 5.14.3 + '@photo-sphere-viewer/core': 5.15.0 '@photostructure/tz-lookup@11.5.0': resolution: {integrity: sha512-0DVFriinZ7TeOnm9ytXeSL3NMFU87ZqMjgbPNkd8LgHFLcPg1BDyM1eewFYs+pPM+62S4fSP9Mtgijmn+6y95w==} @@ -4140,9 +4140,9 @@ packages: resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} engines: {node: ^14.18.0 || >=16.0.0} - '@playwright/test@1.61.1': - resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==} - engines: {node: '>=18'} + '@playwright/test@1.62.0': + resolution: {integrity: sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==} + engines: {node: '>=20'} hasBin: true '@pnpm/config.env-replace@1.1.0': @@ -4692,8 +4692,8 @@ packages: svelte: ^5.0.0 vite: ^6.3.0 || >=7.0.0 - '@sveltejs/kit@2.69.3': - resolution: {integrity: sha512-cphwqMRcE19/9VkrIPr5qZhQ0SptSSDfDzRUpYHu9OJDFGuYBFyJzK+KQA27wB4YG32O/yF2QjBkDmTyo0vtCw==} + '@sveltejs/kit@2.70.1': + resolution: {integrity: sha512-nY9SPHGOZro3doud9vZXDBwl9tCZIouuJztjgSHs6PAIrv9M/z5O7eOhPV5xU7CgVHA976Jwu3BA1hIFvXztkA==} engines: {node: '>=18.13'} hasBin: true peerDependencies: @@ -4797,86 +4797,86 @@ packages: resolution: {integrity: sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==} engines: {node: '>=14'} - '@swc/core-darwin-arm64@1.15.43': - resolution: {integrity: sha512-v1aVuvXdo/BHxJzco9V2xpHrvwWmhfS8t6gziY5wJxd+Z2h8AeJRnAwPD8itCDaGXVBwJ/CaKfxEzTkG0Va0OA==} + '@swc/core-darwin-arm64@1.15.46': + resolution: {integrity: sha512-IsISIT22EfktVJrlvIpnAxG2u/A9aob9l99HMlx80x72WlFmFPk1V3UhkEzx86eJP8hw049KTFv/RISho2cq2Q==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.15.43': - resolution: {integrity: sha512-lp3d4Lamc8dt5huYdGLSR+9hLxmfr1jb0l+4XXG2zPqZwYWRN9R0U2qYoTrggiU2RWW0oV9VbWM3kBnqIc2kdQ==} + '@swc/core-darwin-x64@1.15.46': + resolution: {integrity: sha512-4Tj4ppVIPCmUMpmGFiGtyEriwLyJ+yi/US4WfBrP/ok8COGddDZXLEzQETnKyK46mjvr1v0jevrS23zjoff7vA==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.15.43': - resolution: {integrity: sha512-JWTQQELtsG5GgphDrr/XqqmM2pDN3cZqbMS0Mrg+iTiXL3F74sn/S2IyYE/5u4h2KLkTf9qQ7dXyxsbx7YzkeA==} + '@swc/core-linux-arm-gnueabihf@1.15.46': + resolution: {integrity: sha512-i8tUGnNjyOgMmfmgFSg4aeJLQoFyfpIHK5FjpQAwpRyQIqEUB2w1e8zIDQzY1WhOxx8NoS1S5iUL813Un4Sf5A==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.15.43': - resolution: {integrity: sha512-B4otJRdPWIsmiSBf0uG7Z/+vMWmkufjz5MmYxubwKuZazDW14Zd3symga1N62QR4RT+kEFeHEgsXfZGyn/w0hw==} + '@swc/core-linux-arm64-gnu@1.15.46': + resolution: {integrity: sha512-c0OnhqzdhfOvv6qhNCcByepB+sNYOGZyhtr2Qa6ZCHvAWTYhSRw4j/u92Stue9PbZ/6q74b9nHzi76+kVzqQHQ==} engines: {node: '>=10'} cpu: [arm64] os: [linux] libc: [glibc] - '@swc/core-linux-arm64-musl@1.15.43': - resolution: {integrity: sha512-6zB6OnpViBxYy4tgY3v2i6AZY9fwkcHZ032UOwtwUuW1d19sdT07qF0kZe6/3UR1tUaK6jjg2rmVcUIBCEYVjQ==} + '@swc/core-linux-arm64-musl@1.15.46': + resolution: {integrity: sha512-imyRpNEcUzFQFV2LE4jL68ErvmKEuZCbvZru77iQREunJ+bR4i658cupTgtG1mLYM3F1Tzy3Sb9xYb02KghWTg==} engines: {node: '>=10'} cpu: [arm64] os: [linux] libc: [musl] - '@swc/core-linux-ppc64-gnu@1.15.43': - resolution: {integrity: sha512-coxE1ZWdB3uSDVNoEtYNrRi/1epvckZx9cTJ8ICUxTMTxGk+yvQ/Twacp3ruZSaMPGCriUjP86C37VhaT6nyRg==} + '@swc/core-linux-ppc64-gnu@1.15.46': + resolution: {integrity: sha512-ctEfcl/HcUeomK33cbySiHZm98GEDIxTm1EkpBsYCiHxElYBzvTXVeuQT2YwbUXn9XCrjiw4ipyUNk33k26qRg==} engines: {node: '>=10'} cpu: [ppc64] os: [linux] libc: [glibc] - '@swc/core-linux-s390x-gnu@1.15.43': - resolution: {integrity: sha512-lXfLhs+LpBsD5inuYx+YDH5WsPPBQ95KPUiy8P5wq9ob9xKDZFqwNfU2QW6bGO8NqRO/H9JQomTSt5Yyh+FGfA==} + '@swc/core-linux-s390x-gnu@1.15.46': + resolution: {integrity: sha512-DxlMdnt84TtRVTv7WL/thWyz9+QU8QZNNoAP9rrk0P68LziuhfePp8MjQ44zIprpTHTsEwyziIuGUUN5iSC1bQ==} engines: {node: '>=10'} cpu: [s390x] os: [linux] libc: [glibc] - '@swc/core-linux-x64-gnu@1.15.43': - resolution: {integrity: sha512-07XnKwTmKy8TGOZG3D9fRnLWGynxPjwQnZLVmBFbo6F+7vHYzBIOuwXEhemrChBWb6yDNZsVCcMWCPX6FDD2xg==} + '@swc/core-linux-x64-gnu@1.15.46': + resolution: {integrity: sha512-SKxI7J6t90XPl8hRUqtJi9NfGdunN/E/vZMc7Bc0figeRdOPDBT+Tm8g7cx9xM0T0mewh2l+8dewa3Am27/P+A==} engines: {node: '>=10'} cpu: [x64] os: [linux] libc: [glibc] - '@swc/core-linux-x64-musl@1.15.43': - resolution: {integrity: sha512-TJc+bsSIaBh+hZvZ5GRtW/K1bw66TJ9vsUwvVIsZdiWxU5ObLwZvfcnZ3UpgVfMnFibRes9uriJrQNBHEEogRQ==} + '@swc/core-linux-x64-musl@1.15.46': + resolution: {integrity: sha512-qj9T6B7bosI0VEsrWOVXZN1OXxS8Tp63ywyrLxNdOycnUtLdkgYcoBsN5y8ImnDDsnwrEWZOy1e+J4xSe7mA3Q==} engines: {node: '>=10'} cpu: [x64] os: [linux] libc: [musl] - '@swc/core-win32-arm64-msvc@1.15.43': - resolution: {integrity: sha512-jfd7s2/bUQYkOHLs+LWQNKZdmDa8+sufKLllhpWAhVQ2GDCwsHe3vR/j+OSiItZNtkzFuaawa3+SAKz9y5gYfw==} + '@swc/core-win32-arm64-msvc@1.15.46': + resolution: {integrity: sha512-8p7l4c3LU+eA5g9Et1JPhNeMC1oQwXTGU+uah8DPIBX7YXzqswvaBtyKVmXefVGi/DJU1x3YJsc3mbAp9aWzSQ==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.15.43': - resolution: {integrity: sha512-rLAE8JvucqEW1ZGohxPQrQWPBQeJG4+ypKbWfdlU/qmKScvCkxf9/Jxnzki1dkUQCQ7P5Enp13RlvqOlvx/32g==} + '@swc/core-win32-ia32-msvc@1.15.46': + resolution: {integrity: sha512-tUEnfr3Bn9u6FOjUb3PN9p+09qZC2j+wNDLKHzXXZn22rqGcUqR/ohCRSS+nG9B9+X+U+3FewNEHJkTmdIvMjQ==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.15.43': - resolution: {integrity: sha512-h8MLDHZcfIukwQWj03rIJZx1I0E81AYj2X7J/nGErG4nz+QAv6G1Z+peotvinL3lqpbo32tLYSMFo32/ySzxKg==} + '@swc/core-win32-x64-msvc@1.15.46': + resolution: {integrity: sha512-Vux7UDzBJYQggSuPfcl2w9iu+IJpgpRCxHzgCaVkELnAXAE4XZMOTX9HNcaNiwfeIDqdu2rkr69RuDm6wY8neA==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.15.43': - resolution: {integrity: sha512-1CuKjFkPxIgGdeHVuNbkxmBxkcbdc08u0aiI43pFq6yY1tTVKmXT9hFEooyyKs/sJ3xf1GPHyEwTtk9Xl8dvQw==} + '@swc/core@1.15.46': + resolution: {integrity: sha512-Ri3em2mBpq3h2zSPliCYl63otDGqek8PPEfv2nWgRQEbZ/VBCNyypVTVQ6cEbTCXBhy+WE2T3fQb08moIyuYaw==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -4897,69 +4897,69 @@ packages: resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} - '@tailwindcss/node@4.3.2': - resolution: {integrity: sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==} + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} - '@tailwindcss/oxide-android-arm64@4.3.2': - resolution: {integrity: sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==} + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} engines: {node: '>= 20'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.3.2': - resolution: {integrity: sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==} + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} engines: {node: '>= 20'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.3.2': - resolution: {integrity: sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==} + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} engines: {node: '>= 20'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.3.2': - resolution: {integrity: sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==} + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} engines: {node: '>= 20'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2': - resolution: {integrity: sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} engines: {node: '>= 20'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.3.2': - resolution: {integrity: sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==} + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-arm64-musl@4.3.2': - resolution: {integrity: sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==} + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [musl] - '@tailwindcss/oxide-linux-x64-gnu@4.3.2': - resolution: {integrity: sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==} + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-x64-musl@4.3.2': - resolution: {integrity: sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==} + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [musl] - '@tailwindcss/oxide-wasm32-wasi@4.3.2': - resolution: {integrity: sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==} + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -4970,24 +4970,24 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.3.2': - resolution: {integrity: sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==} + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} engines: {node: '>= 20'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.3.2': - resolution: {integrity: sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==} + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} engines: {node: '>= 20'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.3.2': - resolution: {integrity: sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==} + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} engines: {node: '>= 20'} - '@tailwindcss/vite@4.3.2': - resolution: {integrity: sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==} + '@tailwindcss/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 @@ -5505,63 +5505,63 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typescript-eslint/eslint-plugin@8.64.0': - resolution: {integrity: sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==} + '@typescript-eslint/eslint-plugin@8.65.0': + resolution: {integrity: sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.64.0 + '@typescript-eslint/parser': ^8.65.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.64.0': - resolution: {integrity: sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==} + '@typescript-eslint/parser@8.65.0': + resolution: {integrity: sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.64.0': - resolution: {integrity: sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==} + '@typescript-eslint/project-service@8.65.0': + resolution: {integrity: sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.64.0': - resolution: {integrity: sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==} + '@typescript-eslint/scope-manager@8.65.0': + resolution: {integrity: sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.64.0': - resolution: {integrity: sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==} + '@typescript-eslint/tsconfig-utils@8.65.0': + resolution: {integrity: sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.64.0': - resolution: {integrity: sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==} + '@typescript-eslint/type-utils@8.65.0': + resolution: {integrity: sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.64.0': - resolution: {integrity: sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==} + '@typescript-eslint/types@8.65.0': + resolution: {integrity: sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.64.0': - resolution: {integrity: sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==} + '@typescript-eslint/typescript-estree@8.65.0': + resolution: {integrity: sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.64.0': - resolution: {integrity: sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==} + '@typescript-eslint/utils@8.65.0': + resolution: {integrity: sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.64.0': - resolution: {integrity: sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==} + '@typescript-eslint/visitor-keys@8.65.0': + resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript/typescript-aix-ppc64@7.0.2': @@ -5864,8 +5864,8 @@ packages: resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} engines: {node: '>=0.4.0'} - acorn@8.17.0: - resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} engines: {node: '>=0.4.0'} hasBin: true @@ -6240,9 +6240,9 @@ packages: brace-expansion@2.1.1: resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} - brace-expansion@5.0.7: - resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} - engines: {node: 18 || 20 || >=22} + brace-expansion@5.0.8: + resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} + engines: {node: 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} @@ -6281,8 +6281,8 @@ packages: resolution: {integrity: sha512-02yxLeyxF4dNl6SlY6/5HfRSrSdZ/sCPoxy2kZNP5dZZX8LSAD9aE2gtJIUgWrsQTiMPl3mxESyrobSwvRGisQ==} engines: {node: '>=18.20'} - bullmq@5.80.5: - resolution: {integrity: sha512-3cVpkFXvmi7U7clGQGqlIzWLNJgX77Q0U0xCfbsiu6CFdiAcrKem1rK+xHIq0jIzMA6swq9ss6uvMqzCeTbAgg==} + bullmq@5.81.2: + resolution: {integrity: sha512-Hi9GaVCC6HE9bQP65j/FNv1aL1fcEukTF99ezS5pl1Ud+joCFpNWiPCV45mWdkEmpuacS0XJdMMFGKPIHCwoPg==} engines: {node: '>=12.22.0'} peerDependencies: redis: '>=5.0.0' @@ -6799,8 +6799,8 @@ packages: resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} engines: {node: '>= 14'} - crelt@1.0.6: - resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + crelt@1.0.7: + resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==} cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} @@ -7265,8 +7265,8 @@ packages: engines: {node: '>= 16.0.0'} hasBin: true - devalue@5.8.1: - resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} + devalue@5.8.2: + resolution: {integrity: sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA==} devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -7437,12 +7437,8 @@ packages: resolution: {integrity: sha512-2agL3ueZhqxoVrfmntO8yuVj+uNSlIOnhykYHk3Cq0ShYPdUjjUiSJrQvXjq01I9jAuI0Zl2YO8Evv5Mqytm5g==} engines: {node: '>=10.2.0'} - enhanced-resolve@5.21.6: - resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} - engines: {node: '>=10.13.0'} - - enhanced-resolve@5.24.2: - resolution: {integrity: sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==} + enhanced-resolve@5.24.4: + resolution: {integrity: sha512-GVoi+ICHocoOIU7qVVM48wOJziRsqrsyqlI0Ce0LdowRn6v3bcH2zUa9kp85ncx0nwIb9/HOCOLS3fdThDG/XQ==} engines: {node: '>=10.13.0'} entities@2.2.0: @@ -7560,8 +7556,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-better-tailwindcss@4.6.1: - resolution: {integrity: sha512-Lr8mPyuaZ+dS6ATuJaPwcOFOpOUsRBs5TXyOPqiTzLy0SvK4+0G6usbklCuQn4QabwFtNKdSXwl2WxOIPvu0lQ==} + eslint-plugin-better-tailwindcss@4.7.0: + resolution: {integrity: sha512-lrdlVW4pzLPj/zX5HRqMhKPesGijDfRhnSRJMlNcsrCyJHsndmHCLKTiRNa1eREUZ6G3D3QjdLc+G4tlfGrmkw==} engines: {node: ^20.19.0 || ^22.12.0 || >=23.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 @@ -7593,8 +7589,8 @@ packages: eslint-config-prettier: optional: true - eslint-plugin-svelte@3.20.0: - resolution: {integrity: sha512-AElKLVt7Hjy4d7ljwhrhw9hux60DCxCNkmK8cY/aAXvjs8tpR7PvU4DlyI/SA1PaJww1gh0wPGo2pbyURuEwxQ==} + eslint-plugin-svelte@3.22.0: + resolution: {integrity: sha512-O3qn0NePTWta+1o25dIThqeEP/hEQ3VxDK2LVO8SQ5wG9umLMvulK+m1yQ4JGOb2Pkl8IB0G1lpRV/HXDXSLTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.1 || ^9.0.0 || ^10.0.0 @@ -7633,8 +7629,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.7.0: - resolution: {integrity: sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==} + eslint@10.8.0: + resolution: {integrity: sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -7827,8 +7823,8 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fast-uri@3.1.3: - resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} + fast-uri@3.1.4: + resolution: {integrity: sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==} fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -7917,8 +7913,8 @@ packages: resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} hasBin: true - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + flatted@3.4.3: + resolution: {integrity: sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==} fluent-ffmpeg@2.1.3: resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==} @@ -8180,8 +8176,8 @@ packages: engines: {node: '>=0.4.7'} hasBin: true - happy-dom@20.10.6: - resolution: {integrity: sha512-6QD0ilzDDt93tX44y8tbmZdAcdTRYDhUP+Asgi6pC8Pp5IA3cvaZGyoVN/EGtlq9ziT65iPuBBn3ASLr6hCgVw==} + happy-dom@20.11.1: + resolution: {integrity: sha512-XSt8tMzbW9ymE7687xztkO1ckR7qJNQ3LywY9vlYGhGi3zXrGBHuUo2Cl1ztZaICW+1eAGdkLbj6iwVqDT33kg==} engines: {node: '>=20.0.0'} has-flag@4.0.0: @@ -8448,8 +8444,8 @@ packages: immediate@3.3.0: resolution: {integrity: sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==} - immutable-json-patch@6.0.2: - resolution: {integrity: sha512-KwCA5DXJiyldda8SPha1zB+6+vbEi5/jRRcYii/6yFXlyu9ZjiSH/wPq8Ri2Hk8iGjjTMcHW3Z21S4MOpl7sOw==} + immutable-json-patch@6.0.3: + resolution: {integrity: sha512-qFLRzQLjteiTGy5tRJcYqvuu0r/GdQ1fE27IyNBrJ/k7yGSO0xFr6QX62TtmIqSvjQF3afcNFUHsGPYW7ydaHQ==} immutable@5.1.6: resolution: {integrity: sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==} @@ -8780,8 +8776,8 @@ packages: joi@17.13.4: resolution: {integrity: sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ==} - jose@6.2.3: - resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + jose@6.2.4: + resolution: {integrity: sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==} js-tokens@10.0.0: resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} @@ -8796,6 +8792,10 @@ packages: resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} hasBin: true + js-yaml@5.2.1: + resolution: {integrity: sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==} + hasBin: true + jsdom@26.1.0: resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} engines: {node: '>=18'} @@ -8854,8 +8854,8 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - jsonrepair@3.14.0: - resolution: {integrity: sha512-tWPGKMZf/8UPim+fcW2EfcQ/d/7aKUrP6IECz9G3Tu6Q5dX0orSleqJ9z6sSw7qrQkjF8/Edo4DvsWBZ8H+HNg==} + jsonrepair@3.15.0: + resolution: {integrity: sha512-wy8OTjwsJwQRnQJkKnMJJ9vcytRdBPAgIF/Hy6+s1dAj42BHMKiyL8JzEieIl3JY7idt8eyHwBWTO8mh/+mtwA==} hasBin: true jsonwebtoken@9.0.3: @@ -9352,8 +9352,8 @@ packages: mdn-data@2.0.30: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} - mdn-data@2.28.1: - resolution: {integrity: sha512-U9w+PzSZ00Z5m9rZ5ARVFL5xOfuCHdKYi/1RRwDCJsboFgJDNT3zT6PIPD7mZQYaQLhsZM3GfDRgSMRHhSmVng==} + mdn-data@2.29.0: + resolution: {integrity: sha512-pVxQFCcaYUEAH853+v7yoI/qzhxXSq1bTb9obMYGYAN1c3Hen+XDCEvr296XhstrwlSTNgOR7mCSD4JPjbJe5A==} media-chrome@4.19.2: resolution: {integrity: sha512-4ai1ITN8wBhwugQcRgqe3tN0z6OSKGOXqHLNrS04MgKFfsLqu6Dm8MPq02pI9Y9ZKoXtFjIl85jOryIW9es3BA==} @@ -9596,8 +9596,8 @@ packages: minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} - minimatch@10.2.5: - resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} minimatch@3.1.5: @@ -9761,9 +9761,9 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanoid@5.1.16: - resolution: {integrity: sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==} - engines: {node: ^18 || >=20} + nanoid@6.0.0: + resolution: {integrity: sha512-mkUH+rPkwU2qPadJ0oJZOjeZ5Mxn8Q1UhevwkTRWNuUZzyia3h4rhzK39hxaHTk0o2OxB8W2SQ6A8k23ZDi1pQ==} + engines: {node: ^22 || ^24 || >=26} hasBin: true napi-build-utils@2.0.0: @@ -9826,8 +9826,8 @@ packages: '@nestjs/core': '>= 11 < 12' rxjs: ^7.1.0 - nestjs-zod@5.4.0: - resolution: {integrity: sha512-dxVpy1fjfK4kp+ztK+7xQP46fpvZxkeR/jcEdIvEGh/2o71iwXuy/hrKOWSPhJ1nQXV4iBdHqMizndn2GTaXDg==} + nestjs-zod@5.5.0: + resolution: {integrity: sha512-V0WMtmICygYE2RoMKk8ExEeoetkC7PoUmUlZNP4JNk1YgrnUnG1vgkRn2DBNqsDi+Obv3c9AOssmXe/k/AvBOg==} peerDependencies: '@nestjs/common': ^10.0.0 || ^11.0.0 '@nestjs/swagger': ^7.4.2 || ^8.0.0 || ^11.0.0 @@ -9986,8 +9986,8 @@ packages: resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} engines: {node: '>=12.20.0'} - oidc-provider@9.9.1: - resolution: {integrity: sha512-2kvtykfLu3FJSzaIStg8Dz58/pLtAwI3j/vLdy7KXJBDuJJAEgZhOSWklc+MTjKBLlQjeFwBXpKoE0Pf23pnIQ==} + oidc-provider@9.10.0: + resolution: {integrity: sha512-Olmg6oxgHIviZnrf9yaey6vdaWG1f19UiyvpSj0gCgj6wl9YMcOw3uDv3FHj+x62EXeFj7GfZeTL09MCgIEKnw==} on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} @@ -10290,14 +10290,14 @@ packages: resolution: {integrity: sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==} engines: {node: '>=16.0.0'} - playwright-core@1.61.1: - resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} - engines: {node: '>=18'} + playwright-core@1.62.0: + resolution: {integrity: sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==} + engines: {node: '>=20'} hasBin: true - playwright@1.61.1: - resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} - engines: {node: '>=18'} + playwright@1.62.0: + resolution: {integrity: sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==} + engines: {node: '>=20'} hasBin: true plimit-lit@1.6.1: @@ -10778,8 +10778,8 @@ packages: peerDependencies: postcss: ^8.4.31 - postcss@8.5.19: - resolution: {integrity: sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==} + postcss@8.5.25: + resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -10846,8 +10846,8 @@ packages: prettier: ^3.0.0 svelte: ^5.0.0 - prettier@3.9.5: - resolution: {integrity: sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==} + prettier@3.9.6: + resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} engines: {node: '>=14'} hasBin: true @@ -10998,6 +10998,10 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} + raw-body@4.0.0: + resolution: {integrity: sha512-TMHtwexrgOt9VJ2E5JF9RO8mRXGNgC5wXvKkc5AVSJUt1L5gxvjg7eiCQl96EqUEpCWrnNEkCnbAplYtyuq1IA==} + engines: {node: '>=22'} + raw-loader@4.0.2: resolution: {integrity: sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==} engines: {node: '>= 10.13.0'} @@ -11008,10 +11012,10 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true - react-dom@19.2.7: - resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + react-dom@19.2.8: + resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} peerDependencies: - react: ^19.2.7 + react: ^19.2.8 react-email@5.2.11: resolution: {integrity: sha512-9TzTGRGeavli/iv1RICVOePnFOeG2YRyr8kAyXj6Zgudteq60uA0txDwe4q1zIjQ+08fcbzBkSnI9HgPkSK5OA==} @@ -11056,8 +11060,8 @@ packages: peerDependencies: react: '>=15' - react@19.2.7: - resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + react@19.2.8: + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} engines: {node: '>=0.10.0'} read-cache@1.0.0: @@ -11354,8 +11358,8 @@ packages: sanitize-filename@1.6.4: resolution: {integrity: sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==} - sass@1.101.0: - resolution: {integrity: sha512-OL3GoQyoUdDt843DpVmDO6y2k1sc5IhUDSpu8XucEI+35neq5QivZ1iuegnpraEVTJXlQGK1gl27zKcTLEPbQw==} + sass@1.102.0: + resolution: {integrity: sha512-NSOyTnaQF7rTAEOtI2fwb386vL+akyiQLBZu8Na7hXCb+umJy0GAqlcMIaqACZ6Z1VgTBS4K9PG6B3IdjHGJsw==} engines: {node: '>=20.19.0'} hasBin: true @@ -11509,8 +11513,8 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - simple-icons@16.26.0: - resolution: {integrity: sha512-T9rNJtyOshULM8heLlvrZY346g9zOgZILQ3vxP+FWcX13RhaOLez7YM/hNCEx3b5gJckSCNDoNsjZuDncSMYSQ==} + simple-icons@16.27.1: + resolution: {integrity: sha512-slZF8iKxkv7Lb9SF3L1AcIl5iYLNvDrKKm8yV69cG66XvxJ12SX+bYTgxRwCY4bXSmklGXoXaKEyVmEypOCeqw==} engines: {node: '>=0.12.18'} sirv@2.0.4: @@ -11816,8 +11820,8 @@ packages: peerDependencies: svelte: ^3 || ^4 || ^5 - svelte-jsoneditor@3.12.0: - resolution: {integrity: sha512-BUWsAmmDbQTs4AAMvVGA09X1aP8l1kr7dDdzbLVPX8895Ov52PVoAlAsCXERPh+GIan+qZTEBqwSqbM3VRMBaw==} + svelte-jsoneditor@3.13.0: + resolution: {integrity: sha512-4L7jqVulv180NytA12jQfFguUKrkHjQ0KJkrSbxhC7mWZL6tDzcsRKqXNtfCgy9c5CKC340xJmUClVF3VIsONg==} peerDependencies: svelte: ^5.0.0 @@ -11859,8 +11863,8 @@ packages: peerDependencies: svelte: ^5.30.2 - svelte@5.56.5: - resolution: {integrity: sha512-P03YJmUy2JoOxYHb4Ka3oFat1hq2ko2it1MOItSjsJ4B6WgZPLc3+RyyU+57OGWhs3Ieq74EJpZtSnNXdAGgDw==} + svelte@5.56.8: + resolution: {integrity: sha512-PY8LOw7xP6c8IOiVqdo0sbbZVYhXRSfklOQLAUyGBKqjTX0wx/z4l/9J+PmBpmlLnxzEb1NqltxQ5/wZme/Cmg==} engines: {node: '>=18'} svg-parser@2.0.4: @@ -11942,8 +11946,8 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - tailwindcss@4.3.2: - resolution: {integrity: sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==} + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} tapable@2.3.3: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} @@ -12045,9 +12049,6 @@ packages: peerDependencies: tslib: ^2 - three@0.184.0: - resolution: {integrity: sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==} - three@0.185.1: resolution: {integrity: sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==} @@ -12275,8 +12276,8 @@ packages: typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - typescript-eslint@8.64.0: - resolution: {integrity: sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==} + typescript-eslint@8.65.0: + resolution: {integrity: sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -13269,20 +13270,20 @@ snapshots: '@babel/compat-data@7.29.7': {} - '@babel/core@7.29.7': + '@babel/core@7.29.7(supports-color@8.1.1)': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.7 '@babel/helper-compilation-targets': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helpers': 7.29.7 '@babel/parser': 7.29.7 '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) '@babel/types': 7.29.7 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -13309,32 +13310,32 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)': + '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7(supports-color@8.1.1) '@babel/helper-optimise-call-expression': 7.29.7 - '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@8.1.1) + '@babel/traverse': 7.29.7(supports-color@8.1.1) semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/helper-create-regexp-features-plugin@7.29.7(@babel/core@7.29.7)': + '@babel/helper-create-regexp-features-plugin@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.29.7 regexpu-core: 6.4.0 semver: 6.3.1 - '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.7)': + '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) lodash.debounce: 4.0.8 resolve: 1.22.12 transitivePeerDependencies: @@ -13342,26 +13343,26 @@ snapshots: '@babel/helper-globals@7.29.7': {} - '@babel/helper-member-expression-to-functions@7.29.7': + '@babel/helper-member-expression-to-functions@7.29.7(supports-color@8.1.1)': dependencies: - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-module-imports@7.29.7': + '@babel/helper-module-imports@7.29.7(supports-color@8.1.1)': dependencies: - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-module-imports': 7.29.7(supports-color@8.1.1) '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -13371,27 +13372,27 @@ snapshots: '@babel/helper-plugin-utils@7.29.7': {} - '@babel/helper-remap-async-to-generator@7.29.7(@babel/core@7.29.7)': + '@babel/helper-remap-async-to-generator@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-wrap-function': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/helper-wrap-function': 7.29.7(supports-color@8.1.1) + '@babel/traverse': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)': + '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-member-expression-to-functions': 7.29.7(supports-color@8.1.1) '@babel/helper-optimise-call-expression': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + '@babel/helper-skip-transparent-expression-wrappers@7.29.7(supports-color@8.1.1)': dependencies: - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color @@ -13402,10 +13403,10 @@ snapshots: '@babel/helper-validator-option@7.29.7': {} - '@babel/helper-wrap-function@7.29.7': + '@babel/helper-wrap-function@7.29.7(supports-color@8.1.1)': dependencies: '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color @@ -13423,578 +13424,578 @@ snapshots: dependencies: '@babel/types': 7.29.7 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 - '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@8.1.1) + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.7)': + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.29.7)': + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-import-assertions@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-syntax-import-assertions@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.7)': + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-arrow-functions@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-arrow-functions@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-async-generator-functions@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-async-generator-functions@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7) - '@babel/traverse': 7.29.7 + '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/traverse': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-async-to-generator@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-async-to-generator@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-module-imports': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7) + '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-block-scoped-functions@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-block-scoped-functions@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-block-scoping@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-block-scoping@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-class-properties@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-class-properties@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-class-static-block@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-class-static-block@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-classes@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-classes@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.29.7 '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-globals': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) - '@babel/traverse': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/traverse': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-computed-properties@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-computed-properties@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 '@babel/template': 7.29.7 - '@babel/plugin-transform-destructuring@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-destructuring@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-dotall-regex@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-dotall-regex@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-duplicate-keys@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-duplicate-keys@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-dynamic-import@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-dynamic-import@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-explicit-resource-management@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-explicit-resource-management@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-exponentiation-operator@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-exponentiation-operator@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-export-namespace-from@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-export-namespace-from@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-for-of@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-for-of@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-function-name@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-function-name@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-json-strings@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-json-strings@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-literals@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-literals@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-logical-assignment-operators@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-logical-assignment-operators@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-member-expression-literals@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-member-expression-literals@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-modules-amd@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-modules-amd@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-systemjs@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-modules-systemjs@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-umd@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-modules-umd@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-named-capturing-groups-regex@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-named-capturing-groups-regex@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-new-target@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-new-target@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-nullish-coalescing-operator@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-nullish-coalescing-operator@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-numeric-separator@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-numeric-separator@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-object-rest-spread@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-object-rest-spread@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7) - '@babel/traverse': 7.29.7 + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/traverse': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-object-super@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-object-super@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-optional-catch-binding@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-optional-catch-binding@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-optional-chaining@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-optional-chaining@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-parameters@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-parameters@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-private-methods@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-private-methods@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-private-property-in-object@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-private-property-in-object@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-property-literals@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-property-literals@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-react-constant-elements@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-react-constant-elements@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-react-display-name@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-react-display-name@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-react-jsx-development@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-react-jsx-development@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-jsx@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-react-jsx@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-module-imports': 7.29.7 + '@babel/helper-module-imports': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-pure-annotations@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-react-pure-annotations@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-regenerator@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-regenerator@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-regexp-modifiers@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-regexp-modifiers@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-reserved-words@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-reserved-words@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-runtime@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-runtime@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-module-imports': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.7) - babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.7) - babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.7) + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-shorthand-properties@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-shorthand-properties@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-spread@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-spread@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-sticky-regex@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-sticky-regex@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-template-literals@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-template-literals@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-typeof-symbol@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-typeof-symbol@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 - '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@8.1.1) + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-unicode-escapes@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-unicode-escapes@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-unicode-property-regex@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-unicode-property-regex@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-unicode-regex@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-unicode-regex@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-unicode-sets-regex@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-unicode-sets-regex@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.29.7 - '@babel/preset-env@7.29.7(@babel/core@7.29.7)': + '@babel/preset-env@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: '@babel/compat-data': 7.29.7 - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 '@babel/helper-validator-option': 7.29.7 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.7) - '@babel/plugin-syntax-import-assertions': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.7) - '@babel/plugin-transform-arrow-functions': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-async-generator-functions': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-async-to-generator': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-block-scoped-functions': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-block-scoping': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-class-static-block': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-classes': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-computed-properties': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-dotall-regex': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-duplicate-keys': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-dynamic-import': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-explicit-resource-management': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-exponentiation-operator': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-export-namespace-from': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-for-of': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-function-name': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-json-strings': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-literals': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-logical-assignment-operators': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-member-expression-literals': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-modules-amd': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-modules-systemjs': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-modules-umd': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-new-target': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-numeric-separator': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-object-rest-spread': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-object-super': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-optional-catch-binding': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-private-methods': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-private-property-in-object': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-property-literals': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-regenerator': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-regexp-modifiers': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-reserved-words': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-shorthand-properties': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-spread': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-sticky-regex': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-template-literals': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-typeof-symbol': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-unicode-escapes': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-unicode-property-regex': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-unicode-sets-regex': 7.29.7(@babel/core@7.29.7) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.7) - babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.7) - babel-plugin-polyfill-corejs3: 0.14.2(@babel/core@7.29.7) - babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.7) + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-import-assertions': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-arrow-functions': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-async-generator-functions': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-async-to-generator': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-block-scoped-functions': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-block-scoping': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-class-static-block': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-classes': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-computed-properties': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-dotall-regex': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-duplicate-keys': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-dynamic-import': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-explicit-resource-management': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-exponentiation-operator': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-export-namespace-from': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-for-of': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-function-name': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-json-strings': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-literals': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-logical-assignment-operators': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-member-expression-literals': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-modules-amd': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-modules-systemjs': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-modules-umd': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-new-target': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-numeric-separator': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-object-rest-spread': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-object-super': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-optional-catch-binding': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-private-methods': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-private-property-in-object': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-property-literals': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-regenerator': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-regexp-modifiers': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-reserved-words': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-shorthand-properties': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-spread': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-sticky-regex': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-template-literals': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-typeof-symbol': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-unicode-escapes': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-unicode-property-regex': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-unicode-sets-regex': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.7(supports-color@8.1.1)) + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + babel-plugin-polyfill-corejs3: 0.14.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) core-js-compat: 3.49.0 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.7)': + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 '@babel/types': 7.29.7 esutils: 2.0.3 - '@babel/preset-react@7.29.7(@babel/core@7.29.7)': + '@babel/preset-react@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 '@babel/helper-validator-option': 7.29.7 - '@babel/plugin-transform-react-display-name': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-react-jsx-development': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-react-pure-annotations': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-display-name': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-react-jsx-development': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-react-pure-annotations': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) transitivePeerDependencies: - supports-color - '@babel/preset-typescript@7.29.7(@babel/core@7.29.7)': + '@babel/preset-typescript@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 '@babel/helper-validator-option': 7.29.7 - '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -14006,19 +14007,19 @@ snapshots: '@babel/parser': 7.29.7 '@babel/types': 7.29.7 - '@babel/traverse@7.27.0': + '@babel/traverse@7.27.0(supports-color@8.1.1)': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.7 '@babel/parser': 7.29.7 '@babel/template': 7.29.7 '@babel/types': 7.29.7 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) globals: 11.12.0 transitivePeerDependencies: - supports-color - '@babel/traverse@7.29.7': + '@babel/traverse@7.29.7(supports-color@8.1.1)': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.7 @@ -14026,7 +14027,7 @@ snapshots: '@babel/parser': 7.29.7 '@babel/template': 7.29.7 '@babel/types': 7.29.7 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -14047,27 +14048,27 @@ snapshots: '@codemirror/autocomplete@6.20.3': dependencies: - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.7 '@lezer/common': 1.5.2 - '@codemirror/commands@6.10.3': + '@codemirror/commands@6.10.4': dependencies: - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.7 '@lezer/common': 1.5.2 '@codemirror/lang-json@6.0.2': dependencies: - '@codemirror/language': 6.12.3 + '@codemirror/language': 6.12.4 '@lezer/json': 1.0.3 - '@codemirror/language@6.12.3': + '@codemirror/language@6.12.4': dependencies: - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.7 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.10 @@ -14075,24 +14076,24 @@ snapshots: '@codemirror/lint@6.9.7': dependencies: - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 - crelt: 1.0.6 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.7 + crelt: 1.0.7 - '@codemirror/search@6.7.0': + '@codemirror/search@6.7.1': dependencies: - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 - crelt: 1.0.6 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.7 + crelt: 1.0.7 - '@codemirror/state@6.6.0': + '@codemirror/state@6.7.1': dependencies: - '@marijn/find-cluster-break': 1.0.2 + '@marijn/find-cluster-break': 1.0.3 - '@codemirror/view@6.43.1': + '@codemirror/view@6.43.7': dependencies: - '@codemirror/state': 6.6.0 - crelt: 1.0.6 + '@codemirror/state': 6.7.1 + crelt: 1.0.7 style-mod: 4.1.3 w3c-keyname: 2.2.8 @@ -14129,272 +14130,272 @@ snapshots: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-alpha-function@1.0.1(postcss@8.5.19)': + '@csstools/postcss-alpha-function@1.0.1(postcss@8.5.25)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 - '@csstools/postcss-cascade-layers@5.0.2(postcss@8.5.19)': + '@csstools/postcss-cascade-layers@5.0.2(postcss@8.5.25)': dependencies: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.4) - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 - '@csstools/postcss-color-function-display-p3-linear@1.0.1(postcss@8.5.19)': + '@csstools/postcss-color-function-display-p3-linear@1.0.1(postcss@8.5.25)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 - '@csstools/postcss-color-function@4.0.12(postcss@8.5.19)': + '@csstools/postcss-color-function@4.0.12(postcss@8.5.25)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 - '@csstools/postcss-color-mix-function@3.0.12(postcss@8.5.19)': + '@csstools/postcss-color-mix-function@3.0.12(postcss@8.5.25)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 - '@csstools/postcss-color-mix-variadic-function-arguments@1.0.2(postcss@8.5.19)': + '@csstools/postcss-color-mix-variadic-function-arguments@1.0.2(postcss@8.5.25)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 - '@csstools/postcss-content-alt-text@2.0.8(postcss@8.5.19)': + '@csstools/postcss-content-alt-text@2.0.8(postcss@8.5.25)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 - '@csstools/postcss-contrast-color-function@2.0.12(postcss@8.5.19)': + '@csstools/postcss-contrast-color-function@2.0.12(postcss@8.5.25)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 - '@csstools/postcss-exponential-functions@2.0.9(postcss@8.5.19)': + '@csstools/postcss-exponential-functions@2.0.9(postcss@8.5.25)': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.19 + postcss: 8.5.25 - '@csstools/postcss-font-format-keywords@4.0.0(postcss@8.5.19)': + '@csstools/postcss-font-format-keywords@4.0.0(postcss@8.5.25)': dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - '@csstools/postcss-gamut-mapping@2.0.11(postcss@8.5.19)': + '@csstools/postcss-gamut-mapping@2.0.11(postcss@8.5.25)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.19 + postcss: 8.5.25 - '@csstools/postcss-gradients-interpolation-method@5.0.12(postcss@8.5.19)': + '@csstools/postcss-gradients-interpolation-method@5.0.12(postcss@8.5.25)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 - '@csstools/postcss-hwb-function@4.0.12(postcss@8.5.19)': + '@csstools/postcss-hwb-function@4.0.12(postcss@8.5.25)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 - '@csstools/postcss-ic-unit@4.0.4(postcss@8.5.19)': + '@csstools/postcss-ic-unit@4.0.4(postcss@8.5.25)': dependencies: - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - '@csstools/postcss-initial@2.0.1(postcss@8.5.19)': + '@csstools/postcss-initial@2.0.1(postcss@8.5.25)': dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - '@csstools/postcss-is-pseudo-class@5.0.3(postcss@8.5.19)': + '@csstools/postcss-is-pseudo-class@5.0.3(postcss@8.5.25)': dependencies: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.4) - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 - '@csstools/postcss-light-dark-function@2.0.11(postcss@8.5.19)': + '@csstools/postcss-light-dark-function@2.0.11(postcss@8.5.25)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 - '@csstools/postcss-logical-float-and-clear@3.0.0(postcss@8.5.19)': + '@csstools/postcss-logical-float-and-clear@3.0.0(postcss@8.5.25)': dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - '@csstools/postcss-logical-overflow@2.0.0(postcss@8.5.19)': + '@csstools/postcss-logical-overflow@2.0.0(postcss@8.5.25)': dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - '@csstools/postcss-logical-overscroll-behavior@2.0.0(postcss@8.5.19)': + '@csstools/postcss-logical-overscroll-behavior@2.0.0(postcss@8.5.25)': dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - '@csstools/postcss-logical-resize@3.0.0(postcss@8.5.19)': + '@csstools/postcss-logical-resize@3.0.0(postcss@8.5.25)': dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - '@csstools/postcss-logical-viewport-units@3.0.4(postcss@8.5.19)': + '@csstools/postcss-logical-viewport-units@3.0.4(postcss@8.5.25)': dependencies: '@csstools/css-tokenizer': 3.0.4 - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 - '@csstools/postcss-media-minmax@2.0.9(postcss@8.5.19)': + '@csstools/postcss-media-minmax@2.0.9(postcss@8.5.25)': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - postcss: 8.5.19 + postcss: 8.5.25 - '@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.5(postcss@8.5.19)': + '@csstools/postcss-media-queries-aspect-ratio-number-values@3.0.5(postcss@8.5.25)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - postcss: 8.5.19 + postcss: 8.5.25 - '@csstools/postcss-nested-calc@4.0.0(postcss@8.5.19)': + '@csstools/postcss-nested-calc@4.0.0(postcss@8.5.25)': dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - '@csstools/postcss-normalize-display-values@4.0.1(postcss@8.5.19)': + '@csstools/postcss-normalize-display-values@4.0.1(postcss@8.5.25)': dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - '@csstools/postcss-oklab-function@4.0.12(postcss@8.5.19)': + '@csstools/postcss-oklab-function@4.0.12(postcss@8.5.25)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 - '@csstools/postcss-position-area-property@1.0.0(postcss@8.5.19)': + '@csstools/postcss-position-area-property@1.0.0(postcss@8.5.25)': dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - '@csstools/postcss-progressive-custom-properties@4.2.1(postcss@8.5.19)': + '@csstools/postcss-progressive-custom-properties@4.2.1(postcss@8.5.25)': dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - '@csstools/postcss-property-rule-prelude-list@1.0.0(postcss@8.5.19)': + '@csstools/postcss-property-rule-prelude-list@1.0.0(postcss@8.5.25)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.19 + postcss: 8.5.25 - '@csstools/postcss-random-function@2.0.1(postcss@8.5.19)': + '@csstools/postcss-random-function@2.0.1(postcss@8.5.25)': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.19 + postcss: 8.5.25 - '@csstools/postcss-relative-color-syntax@3.0.12(postcss@8.5.19)': + '@csstools/postcss-relative-color-syntax@3.0.12(postcss@8.5.25)': dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 - '@csstools/postcss-scope-pseudo-class@4.0.1(postcss@8.5.19)': + '@csstools/postcss-scope-pseudo-class@4.0.1(postcss@8.5.25)': dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 - '@csstools/postcss-sign-functions@1.1.4(postcss@8.5.19)': + '@csstools/postcss-sign-functions@1.1.4(postcss@8.5.25)': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.19 + postcss: 8.5.25 - '@csstools/postcss-stepped-value-functions@4.0.9(postcss@8.5.19)': + '@csstools/postcss-stepped-value-functions@4.0.9(postcss@8.5.25)': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.19 + postcss: 8.5.25 - '@csstools/postcss-syntax-descriptor-syntax-production@1.0.1(postcss@8.5.19)': + '@csstools/postcss-syntax-descriptor-syntax-production@1.0.1(postcss@8.5.25)': dependencies: '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.19 + postcss: 8.5.25 - '@csstools/postcss-system-ui-font-family@1.0.0(postcss@8.5.19)': + '@csstools/postcss-system-ui-font-family@1.0.0(postcss@8.5.25)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.19 + postcss: 8.5.25 - '@csstools/postcss-text-decoration-shorthand@4.0.3(postcss@8.5.19)': + '@csstools/postcss-text-decoration-shorthand@4.0.3(postcss@8.5.25)': dependencies: '@csstools/color-helpers': 5.1.0 - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - '@csstools/postcss-trigonometric-functions@4.0.9(postcss@8.5.19)': + '@csstools/postcss-trigonometric-functions@4.0.9(postcss@8.5.25)': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.19 + postcss: 8.5.25 - '@csstools/postcss-unset-value@4.0.0(postcss@8.5.19)': + '@csstools/postcss-unset-value@4.0.0(postcss@8.5.25)': dependencies: - postcss: 8.5.19 + postcss: 8.5.25 '@csstools/selector-resolve-nested@3.1.0(postcss-selector-parser@7.1.4)': dependencies: @@ -14404,47 +14405,47 @@ snapshots: dependencies: postcss-selector-parser: 7.1.4 - '@csstools/utilities@2.0.0(postcss@8.5.19)': + '@csstools/utilities@2.0.0(postcss@8.5.25)': dependencies: - postcss: 8.5.19 + postcss: 8.5.25 '@discoveryjs/json-ext@0.5.7': {} - '@docsearch/core@4.6.3(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docsearch/core@4.6.3(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': optionalDependencies: '@types/react': 19.2.17 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) '@docsearch/css@4.6.3': {} - '@docsearch/react@4.6.3(@algolia/client-search@5.56.0)(@types/react@19.2.17)(algoliasearch@5.56.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)': + '@docsearch/react@4.6.3(@algolia/client-search@5.56.0)(@types/react@19.2.17)(algoliasearch@5.56.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)': dependencies: '@algolia/autocomplete-core': 1.19.2(@algolia/client-search@5.56.0)(algoliasearch@5.56.0)(search-insights@2.17.3) - '@docsearch/core': 4.6.3(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docsearch/core': 4.6.3(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@docsearch/css': 4.6.3 optionalDependencies: '@types/react': 19.2.17 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) search-insights: 2.17.3 transitivePeerDependencies: - '@algolia/client-search' - algoliasearch - '@docusaurus/babel@3.10.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/babel@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/generator': 7.29.7 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.7) - '@babel/plugin-transform-runtime': 7.29.7(@babel/core@7.29.7) - '@babel/preset-env': 7.29.7(@babel/core@7.29.7) - '@babel/preset-react': 7.29.7(@babel/core@7.29.7) - '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-runtime': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/preset-env': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/preset-react': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/runtime': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) '@docusaurus/logger': 3.10.2 - '@docusaurus/utils': 3.10.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) babel-plugin-dynamic-import-node: 2.3.3 fs-extra: 11.3.6 tslib: 2.8.1 @@ -14466,66 +14467,32 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/babel@3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/bundler@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@babel/core': 7.29.7 - '@babel/generator': 7.29.7 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.7) - '@babel/plugin-transform-runtime': 7.29.7(@babel/core@7.29.7) - '@babel/preset-env': 7.29.7(@babel/core@7.29.7) - '@babel/preset-react': 7.29.7(@babel/core@7.29.7) - '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7) - '@babel/runtime': 7.29.7 - '@babel/traverse': 7.29.7 - '@docusaurus/logger': 3.10.2 - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - babel-plugin-dynamic-import-node: 2.3.3 - fs-extra: 11.3.6 - tslib: 2.8.1 - transitivePeerDependencies: - - '@minify-html/node' - - '@swc/core' - - '@swc/css' - - '@swc/html' - - clean-css - - cssnano - - csso - - esbuild - - html-minifier-terser - - lightningcss - - postcss - - react - - react-dom - - supports-color - - uglify-js - - webpack-cli - - '@docusaurus/bundler@3.10.2(@typescript/typescript6@6.0.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@babel/core': 7.29.7 - '@docusaurus/babel': 3.10.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@docusaurus/babel': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/cssnano-preset': 3.10.2 '@docusaurus/logger': 3.10.2 - '@docusaurus/types': 3.10.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - babel-loader: 9.2.1(@babel/core@7.29.7)(webpack@5.108.4(postcss@8.5.19)) + '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + babel-loader: 9.2.1(@babel/core@7.29.7(supports-color@8.1.1))(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) clean-css: 5.3.3 - copy-webpack-plugin: 11.0.0(webpack@5.108.4(postcss@8.5.19)) - css-loader: 6.11.0(webpack@5.108.4(postcss@8.5.19)) - css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(webpack@5.108.4(postcss@8.5.19)) - cssnano: 6.1.2(postcss@8.5.19) - file-loader: 6.2.0(webpack@5.108.4(postcss@8.5.19)) + copy-webpack-plugin: 11.0.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + css-loader: 6.11.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + cssnano: 6.1.2(postcss@8.5.25) + file-loader: 6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) html-minifier-terser: 7.2.0 - mini-css-extract-plugin: 2.10.2(webpack@5.108.4(postcss@8.5.19)) - null-loader: 4.0.1(webpack@5.108.4(postcss@8.5.19)) - postcss: 8.5.19 - postcss-loader: 7.3.4(@typescript/typescript6@6.0.2)(postcss@8.5.19)(webpack@5.108.4(postcss@8.5.19)) - postcss-preset-env: 10.6.1(postcss@8.5.19) - terser-webpack-plugin: 5.6.1(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(webpack@5.108.4(postcss@8.5.19)) + mini-css-extract-plugin: 2.10.2(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + null-loader: 4.0.1(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + postcss: 8.5.25 + postcss-loader: 7.3.4(@typescript/typescript6@6.0.2)(postcss@8.5.25)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + postcss-preset-env: 10.6.1(postcss@8.5.25) + terser-webpack-plugin: 5.6.1(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.108.4(postcss@8.5.19)))(webpack@5.108.4(postcss@8.5.19)) - webpack: 5.108.4(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19) - webpackbar: 7.0.0(webpack@5.108.4(postcss@8.5.19)) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)))(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + webpackbar: 7.0.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) transitivePeerDependencies: - '@minify-html/node' - '@parcel/css' @@ -14543,16 +14510,16 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/core@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/core@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/babel': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/bundler': 3.10.2(@typescript/typescript6@6.0.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/babel': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/bundler': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/logger': 3.10.2 - '@docusaurus/mdx-loader': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@mdx-js/react': 3.1.1(@types/react@19.2.17)(react@19.2.7) + '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@mdx-js/react': 3.1.1(@types/react@19.2.17)(react@19.2.8) boxen: 6.2.1 chalk: 4.1.2 chokidar: 3.6.0 @@ -14567,28 +14534,28 @@ snapshots: execa: 5.1.1 fs-extra: 11.3.6 html-tags: 3.3.1 - html-webpack-plugin: 5.6.7(webpack@5.108.4(postcss@8.5.19)) + html-webpack-plugin: 5.6.7(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) leven: 3.1.0 lodash: 4.18.1 open: 8.4.2 p-map: 4.0.0 prompts: 2.4.2 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)' - react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.7)' - react-loadable-ssr-addon-v5-slorber: 1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.7))(webpack@5.108.4(postcss@8.5.19)) - react-router: 5.3.4(react@19.2.7) - react-router-config: 5.1.1(react-router@5.3.4(react@19.2.7))(react@19.2.7) - react-router-dom: 5.3.4(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.8)' + react-loadable-ssr-addon-v5-slorber: 1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.8))(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) + react-router: 5.3.4(react@19.2.8) + react-router-config: 5.1.1(react-router@5.3.4(react@19.2.8))(react@19.2.8) + react-router-dom: 5.3.4(react@19.2.8) semver: 7.8.5 serve-handler: 6.1.7 tinypool: 1.1.1 tslib: 2.8.1 update-notifier: 6.0.2 - webpack: 5.108.4(postcss@8.5.19) + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) webpack-bundle-analyzer: 4.10.2 - webpack-dev-server: 5.2.6(tslib@2.8.1)(webpack@5.108.4(postcss@8.5.19)) + webpack-dev-server: 5.2.6(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(tslib@2.8.1)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) webpack-merge: 6.0.1 transitivePeerDependencies: - '@minify-html/node' @@ -14614,9 +14581,9 @@ snapshots: '@docusaurus/cssnano-preset@3.10.2': dependencies: - cssnano-preset-advanced: 6.1.2(postcss@8.5.19) - postcss: 8.5.19 - postcss-sort-media-queries: 5.2.0(postcss@8.5.19) + cssnano-preset-advanced: 6.1.2(postcss@8.5.25) + postcss: 8.5.25 + postcss-sort-media-queries: 5.2.0(postcss@8.5.25) tslib: 2.8.1 '@docusaurus/logger@3.10.2': @@ -14624,34 +14591,34 @@ snapshots: chalk: 4.1.2 tslib: 2.8.1 - '@docusaurus/mdx-loader@3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/mdx-loader@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: '@docusaurus/logger': 3.10.2 - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@mdx-js/mdx': 3.1.1 + '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@mdx-js/mdx': 3.1.1(supports-color@8.1.1) '@slorber/remark-comment': 1.0.0 escape-html: 1.0.3 estree-util-value-to-estree: 3.5.0 - file-loader: 6.2.0(webpack@5.108.4(postcss@8.5.19)) + file-loader: 6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) fs-extra: 11.3.6 image-size: 2.0.2 - mdast-util-mdx: 3.0.0 + mdast-util-mdx: 3.0.0(supports-color@8.1.1) mdast-util-to-string: 4.0.0 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) rehype-raw: 7.0.0 - remark-directive: 3.0.1 + remark-directive: 3.0.1(supports-color@8.1.1) remark-emoji: 4.0.1 - remark-frontmatter: 5.0.0 - remark-gfm: 4.0.1 + remark-frontmatter: 5.0.0(supports-color@8.1.1) + remark-gfm: 4.0.1(supports-color@8.1.1) stringify-object: 3.3.0 tslib: 2.8.1 unified: 11.0.5 unist-util-visit: 5.1.0 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.108.4(postcss@8.5.19)))(webpack@5.108.4(postcss@8.5.19)) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)))(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) vfile: 6.0.3 - webpack: 5.108.4(postcss@8.5.19) + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) transitivePeerDependencies: - '@minify-html/node' - '@swc/core' @@ -14668,17 +14635,17 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/module-type-aliases@3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/module-type-aliases@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@types/history': 4.7.11 '@types/react': 19.2.17 '@types/react-router-config': 5.0.11 '@types/react-router-dom': 5.3.3 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)' - react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.7)' + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' + react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.8)' transitivePeerDependencies: - '@minify-html/node' - '@swc/core' @@ -14695,30 +14662,30 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/plugin-content-blog@3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/plugin-content-blog@3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/logger': 3.10.2 - '@docusaurus/mdx-loader': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) cheerio: 1.0.0-rc.12 combine-promises: 1.2.0 feed: 4.2.2 fs-extra: 11.3.6 lodash: 4.18.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) schema-dts: 1.1.5 srcset: 4.0.0 tslib: 2.8.1 unist-util-visit: 5.1.0 utility-types: 3.11.0 - webpack: 5.108.4(postcss@8.5.19) + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -14743,28 +14710,28 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/logger': 3.10.2 - '@docusaurus/mdx-loader': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/module-type-aliases': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/module-type-aliases': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@types/react-router-config': 5.0.11 combine-promises: 1.2.0 fs-extra: 11.3.6 js-yaml: 4.3.0 lodash: 4.18.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) schema-dts: 1.1.5 tslib: 2.8.1 utility-types: 3.11.0 - webpack: 5.108.4(postcss@8.5.19) + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -14789,18 +14756,18 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-pages@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/plugin-content-pages@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/mdx-loader': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) fs-extra: 11.3.6 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 - webpack: 5.108.4(postcss@8.5.19) + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -14825,12 +14792,12 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-css-cascade-layers@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/plugin-css-cascade-layers@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) tslib: 2.8.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -14858,15 +14825,15 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-debug@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/plugin-debug@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) fs-extra: 11.3.6 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-json-view-lite: 2.5.0(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-json-view-lite: 2.5.0(react@19.2.8) tslib: 2.8.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -14892,13 +14859,13 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-analytics@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/plugin-google-analytics@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -14924,13 +14891,13 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-gtag@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/plugin-google-gtag@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -14956,13 +14923,13 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-tag-manager@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/plugin-google-tag-manager@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -14988,17 +14955,17 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-sitemap@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/plugin-sitemap@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/logger': 3.10.2 - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) fs-extra: 11.3.6 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) sitemap: 7.1.3 tslib: 2.8.1 transitivePeerDependencies: @@ -15025,18 +14992,18 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-svgr@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/plugin-svgr@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@svgr/core': 8.1.0(@typescript/typescript6@6.0.2) - '@svgr/webpack': 8.1.0(@typescript/typescript6@6.0.2) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@svgr/core': 8.1.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@svgr/webpack': 8.1.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 - webpack: 5.108.4(postcss@8.5.19) + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) transitivePeerDependencies: - '@docusaurus/faster' - '@mdx-js/react' @@ -15061,25 +15028,25 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/preset-classic@3.10.2(@algolia/client-search@5.56.0)(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)': + '@docusaurus/preset-classic@3.10.2(@algolia/client-search@5.56.0)(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-content-blog': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-content-pages': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-css-cascade-layers': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-debug': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-google-analytics': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-google-gtag': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-google-tag-manager': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-sitemap': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-svgr': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/theme-classic': 3.10.2(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/theme-search-algolia': 3.10.2(@algolia/client-search@5.56.0)(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3) - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-blog': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-pages': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-css-cascade-layers': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-debug': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-google-analytics': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-google-gtag': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-google-tag-manager': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-sitemap': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-svgr': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/theme-classic': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/theme-search-algolia': 3.10.2(@algolia/client-search@5.56.0)(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) transitivePeerDependencies: - '@algolia/client-search' - '@docusaurus/faster' @@ -15107,38 +15074,38 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/react-loadable@6.0.0(react@19.2.7)': + '@docusaurus/react-loadable@6.0.0(react@19.2.8)': dependencies: '@types/react': 19.2.17 - react: 19.2.7 + react: 19.2.8 - '@docusaurus/theme-classic@3.10.2(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/theme-classic@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/logger': 3.10.2 - '@docusaurus/mdx-loader': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/module-type-aliases': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-content-blog': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-content-pages': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/module-type-aliases': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-blog': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-pages': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/theme-translations': 3.10.2 - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@mdx-js/react': 3.1.1(@types/react@19.2.17)(react@19.2.7) + '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@mdx-js/react': 3.1.1(@types/react@19.2.17)(react@19.2.8) clsx: 2.1.1 copy-text-to-clipboard: 3.2.2 infima: 0.2.0-alpha.45 lodash: 4.18.1 nprogress: 0.2.0 - postcss: 8.5.19 - prism-react-renderer: 2.4.1(react@19.2.7) + postcss: 8.5.25 + prism-react-renderer: 2.4.1(react@19.2.8) prismjs: 1.30.0 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-router-dom: 5.3.4(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-router-dom: 5.3.4(react@19.2.8) rtlcss: 4.3.0 tslib: 2.8.1 utility-types: 3.11.0 @@ -15165,21 +15132,21 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-common@3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/theme-common@3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/mdx-loader': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/module-type-aliases': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/mdx-loader': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/module-type-aliases': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@types/history': 4.7.11 '@types/react': 19.2.17 '@types/react-router-config': 5.0.11 clsx: 2.1.1 parse-numeric-range: 1.3.0 - prism-react-renderer: 2.4.1(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + prism-react-renderer: 2.4.1(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 utility-types: 3.11.0 transitivePeerDependencies: @@ -15198,16 +15165,16 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/theme-mermaid@3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/theme-mermaid@3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/module-type-aliases': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/module-type-aliases': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) mermaid: 11.16.0 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 transitivePeerDependencies: - '@docusaurus/faster' @@ -15234,25 +15201,25 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-search-algolia@3.10.2(@algolia/client-search@5.56.0)(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)': + '@docusaurus/theme-search-algolia@3.10.2(@algolia/client-search@5.56.0)(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: '@algolia/autocomplete-core': 1.19.9(@algolia/client-search@5.56.0)(algoliasearch@5.56.0)(search-insights@2.17.3) - '@docsearch/react': 4.6.3(@algolia/client-search@5.56.0)(@types/react@19.2.17)(algoliasearch@5.56.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3) - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docsearch/react': 4.6.3(@algolia/client-search@5.56.0)(@types/react@19.2.17)(algoliasearch@5.56.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.3) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/logger': 3.10.2 - '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/plugin-content-docs': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/theme-common': 3.10.2(@docusaurus/plugin-content-docs@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) '@docusaurus/theme-translations': 3.10.2 - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-validation': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-validation': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) algoliasearch: 5.56.0 algoliasearch-helper: 3.29.2(algoliasearch@5.56.0) clsx: 2.1.1 eta: 2.2.0 fs-extra: 11.3.6 lodash: 4.18.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) tslib: 2.8.1 utility-types: 3.11.0 transitivePeerDependencies: @@ -15289,19 +15256,19 @@ snapshots: '@docusaurus/tsconfig@3.10.2': {} - '@docusaurus/types@3.10.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/types@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@mdx-js/mdx': 3.1.1 + '@mdx-js/mdx': 3.1.1(supports-color@8.1.1) '@types/history': 4.7.11 '@types/mdast': 4.0.4 '@types/react': 19.2.17 commander: 5.1.0 joi: 17.13.4 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)' + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)' utility-types: 3.11.0 - webpack: 5.108.4(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19) + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) webpack-merge: 5.10.0 transitivePeerDependencies: - '@minify-html/node' @@ -15319,39 +15286,9 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/types@3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/utils-common@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: - '@mdx-js/mdx': 3.1.1 - '@types/history': 4.7.11 - '@types/mdast': 4.0.4 - '@types/react': 19.2.17 - commander: 5.1.0 - joi: 17.13.4 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)' - utility-types: 3.11.0 - webpack: 5.108.4(postcss@8.5.19) - webpack-merge: 5.10.0 - transitivePeerDependencies: - - '@minify-html/node' - - '@swc/core' - - '@swc/css' - - '@swc/html' - - clean-css - - cssnano - - csso - - esbuild - - html-minifier-terser - - lightningcss - - postcss - - supports-color - - uglify-js - - webpack-cli - - '@docusaurus/utils-common@3.10.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@docusaurus/types': 3.10.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) tslib: 2.8.1 transitivePeerDependencies: - '@minify-html/node' @@ -15371,33 +15308,11 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils-common@3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - tslib: 2.8.1 - transitivePeerDependencies: - - '@minify-html/node' - - '@swc/core' - - '@swc/css' - - '@swc/html' - - clean-css - - cssnano - - csso - - esbuild - - html-minifier-terser - - lightningcss - - postcss - - react - - react-dom - - supports-color - - uglify-js - - webpack-cli - - '@docusaurus/utils-validation@3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/utils-validation@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: '@docusaurus/logger': 3.10.2 - '@docusaurus/utils': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/utils': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) fs-extra: 11.3.6 joi: 17.13.4 js-yaml: 4.3.0 @@ -15421,15 +15336,15 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/utils@3.10.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@docusaurus/utils@3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3)': dependencies: '@11ty/gray-matter': 1.0.0 '@docusaurus/logger': 3.10.2 - '@docusaurus/types': 3.10.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/types': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) + '@docusaurus/utils-common': 3.10.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) escape-string-regexp: 4.0.0 execa: 5.1.1 - file-loader: 6.2.0(webpack@5.108.4(postcss@8.5.19)) + file-loader: 6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) fs-extra: 11.3.6 github-slugger: 1.5.0 globby: 11.1.0 @@ -15441,50 +15356,9 @@ snapshots: prompts: 2.4.2 resolve-pathname: 3.0.0 tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.108.4(postcss@8.5.19)))(webpack@5.108.4(postcss@8.5.19)) + url-loader: 4.1.1(file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)))(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) utility-types: 3.11.0 - webpack: 5.108.4(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19) - transitivePeerDependencies: - - '@minify-html/node' - - '@swc/core' - - '@swc/css' - - '@swc/html' - - clean-css - - cssnano - - csso - - esbuild - - html-minifier-terser - - lightningcss - - postcss - - react - - react-dom - - supports-color - - uglify-js - - webpack-cli - - '@docusaurus/utils@3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@11ty/gray-matter': 1.0.0 - '@docusaurus/logger': 3.10.2 - '@docusaurus/types': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@docusaurus/utils-common': 3.10.2(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - escape-string-regexp: 4.0.0 - execa: 5.1.1 - file-loader: 6.2.0(webpack@5.108.4(postcss@8.5.19)) - fs-extra: 11.3.6 - github-slugger: 1.5.0 - globby: 11.1.0 - jiti: 1.21.7 - js-yaml: 4.3.0 - lodash: 4.18.1 - micromatch: 4.0.8 - p-queue: 6.6.2 - prompts: 2.4.2 - resolve-pathname: 3.0.0 - tslib: 2.8.1 - url-loader: 4.1.1(file-loader@6.2.0(webpack@5.108.4(postcss@8.5.19)))(webpack@5.108.4(postcss@8.5.19)) - utility-types: 3.11.0 - webpack: 5.108.4(postcss@8.5.19) + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) transitivePeerDependencies: - '@minify-html/node' - '@swc/core' @@ -15744,22 +15618,22 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.7.0(jiti@2.7.0))': + '@eslint-community/eslint-utils@4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))': dependencies: - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.23.5': + '@eslint/config-array@0.23.5(supports-color@8.1.1)': dependencies: '@eslint/object-schema': 3.0.5 - debug: 4.4.3 - minimatch: 10.2.5 + debug: 4.4.3(supports-color@8.1.1) + minimatch: 10.2.6 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.6.0': + '@eslint/config-helpers@0.7.0': dependencies: '@eslint/core': 1.2.1 @@ -15767,14 +15641,14 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/css-tree@4.0.4': + '@eslint/css-tree@4.0.5': dependencies: - mdn-data: 2.28.1 + mdn-data: 2.29.0 source-map-js: 1.2.1 - '@eslint/js@10.0.1(eslint@10.7.0(jiti@2.7.0))': + '@eslint/js@10.0.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))': optionalDependencies: - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) '@eslint/object-schema@3.0.5': {} @@ -15794,18 +15668,18 @@ snapshots: '@fig/complete-commander@3.2.0(commander@11.1.0)': dependencies: commander: 11.1.0 - prettier: 3.9.5 + prettier: 3.9.6 - '@floating-ui/core@1.7.5': + '@floating-ui/core@1.8.0': dependencies: - '@floating-ui/utils': 0.2.11 + '@floating-ui/utils': 0.2.12 - '@floating-ui/dom@1.7.6': + '@floating-ui/dom@1.8.0': dependencies: - '@floating-ui/core': 1.7.5 - '@floating-ui/utils': 0.2.11 + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 - '@floating-ui/utils@0.2.11': {} + '@floating-ui/utils@0.2.12': {} '@formatjs/ecma402-abstract@2.3.6': dependencies: @@ -15841,20 +15715,20 @@ snapshots: dependencies: tslib: 2.8.1 - '@fortawesome/fontawesome-common-types@7.2.0': {} + '@fortawesome/fontawesome-common-types@7.3.1': {} - '@fortawesome/free-regular-svg-icons@7.2.0': + '@fortawesome/free-regular-svg-icons@7.3.1': dependencies: - '@fortawesome/fontawesome-common-types': 7.2.0 + '@fortawesome/fontawesome-common-types': 7.3.1 - '@fortawesome/free-solid-svg-icons@7.2.0': + '@fortawesome/free-solid-svg-icons@7.3.1': dependencies: - '@fortawesome/fontawesome-common-types': 7.2.0 + '@fortawesome/fontawesome-common-types': 7.3.1 - '@golevelup/nestjs-discovery@5.0.0(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)': + '@golevelup/nestjs-discovery@5.0.0(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)': dependencies: - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) + '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) lodash: 4.18.1 '@grpc/grpc-js@1.14.4': @@ -16013,19 +15887,19 @@ snapshots: pg-connection-string: 2.14.0 postgres: 3.4.9 - '@immich/ui@0.83.0(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.5(@typescript-eslint/types@8.64.0))': + '@immich/ui@0.83.0(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0))': dependencies: '@internationalized/date': 3.12.2 '@mdi/js': 7.4.47 - '@sveltejs/kit': 2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) - bits-ui: 2.18.1(@internationalized/date@3.12.2)(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + '@sveltejs/kit': 2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + bits-ui: 2.18.1(@internationalized/date@3.12.2)(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) luxon: 3.7.2 - simple-icons: 16.26.0 - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + simple-icons: 16.27.1 + svelte: 5.56.8(@typescript-eslint/types@8.65.0) svelte-highlight: 7.11.0 tailwind-merge: 3.6.0 - tailwind-variants: 3.2.2(tailwind-merge@3.6.0)(tailwindcss@4.3.2) - tailwindcss: 4.3.2 + tailwind-variants: 3.2.2(tailwind-merge@3.6.0)(tailwindcss@4.3.3) + tailwindcss: 4.3.3 '@inquirer/ansi@1.0.2': {} @@ -16367,9 +16241,9 @@ snapshots: dependencies: vary: 1.1.2 - '@koa/router@15.7.0(koa@3.2.1)': + '@koa/router@15.7.0(koa@3.2.1)(supports-color@8.1.1)': dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) http-errors: 2.0.1 koa: 3.2.1 koa-compose: 4.1.0 @@ -16377,20 +16251,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@koddsson/eslint-plugin-tscompat@0.2.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0))': + '@koddsson/eslint-plugin-tscompat@0.2.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: '@mdn/browser-compat-data': 6.1.5 - '@typescript-eslint/type-utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) - '@typescript-eslint/utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) + '@typescript-eslint/type-utils': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/utils': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) browserslist: 4.28.6 transitivePeerDependencies: - eslint - supports-color - typescript - '@kwsites/file-exists@1.1.1': + '@kwsites/file-exists@1.1.1(supports-color@8.1.1)': dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -16418,10 +16292,10 @@ snapshots: '@mapbox/mapbox-gl-rtl-text@0.4.0': {} - '@mapbox/node-pre-gyp@1.0.11': + '@mapbox/node-pre-gyp@1.0.11(supports-color@8.1.1)': dependencies: detect-libc: 2.1.2 - https-proxy-agent: 5.0.1 + https-proxy-agent: 5.0.1(supports-color@8.1.1) make-dir: 3.1.0 node-fetch: 2.7.0 nopt: 5.0.0 @@ -16472,7 +16346,7 @@ snapshots: '@types/geojson': 7946.0.16 pbf: 5.1.0 - '@marijn/find-cluster-break@1.0.2': {} + '@marijn/find-cluster-break@1.0.3': {} '@mdi/js@7.4.47': {} @@ -16484,26 +16358,26 @@ snapshots: '@mdn/browser-compat-data@6.1.5': {} - '@mdx-js/mdx@3.1.1': + '@mdx-js/mdx@3.1.1(supports-color@8.1.1)': dependencies: '@types/estree': 1.0.9 '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.5 '@types/mdx': 2.0.14 - acorn: 8.17.0 + acorn: 8.18.0 collapse-white-space: 2.1.0 devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 estree-util-scope: 1.0.0 estree-walker: 3.0.3 - hast-util-to-jsx-runtime: 2.3.6 + hast-util-to-jsx-runtime: 2.3.6(supports-color@8.1.1) markdown-extensions: 2.0.0 recma-build-jsx: 1.0.0 - recma-jsx: 1.0.1(acorn@8.17.0) + recma-jsx: 1.0.1(acorn@8.18.0) recma-stringify: 1.0.0 - rehype-recma: 1.0.0 - remark-mdx: 3.1.1 - remark-parse: 11.0.0 + rehype-recma: 1.0.0(supports-color@8.1.1) + remark-mdx: 3.1.1(supports-color@8.1.1) + remark-parse: 11.0.0(supports-color@8.1.1) remark-rehype: 11.1.2 source-map: 0.7.6 unified: 11.0.5 @@ -16514,11 +16388,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7)': + '@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8)': dependencies: '@types/mdx': 2.0.14 '@types/react': 19.2.17 - react: 19.2.7 + react: 19.2.8 '@mermaid-js/parser@1.2.0': dependencies: @@ -16553,42 +16427,42 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true - '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)': + '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)': dependencies: - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) + '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 - '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(bullmq@5.80.5)': + '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(bullmq@5.81.2(supports-color@8.1.1))': dependencies: - '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28) - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) - bullmq: 5.80.5 + '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28) + '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) + '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + bullmq: 5.81.2(supports-color@8.1.1) tslib: 2.8.1 - '@nestjs/cli@11.0.24(@swc/core@1.15.43(@swc/helpers@0.5.23))(@types/node@24.13.3)(esbuild@0.28.1)(lightningcss@1.33.0)(prettier@3.9.5)': + '@nestjs/cli@11.0.24(@swc/core@1.15.46(@swc/helpers@0.5.23))(@types/node@24.13.3)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(prettier@3.9.6)(uglify-js@3.19.3)': dependencies: '@angular-devkit/core': 19.2.27(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.27(chokidar@4.0.3) '@angular-devkit/schematics-cli': 19.2.27(@types/node@24.13.3)(chokidar@4.0.3) '@inquirer/prompts': 7.10.1(@types/node@24.13.3) - '@nestjs/schematics': 11.1.0(chokidar@4.0.3)(prettier@3.9.5)(typescript@5.9.3) + '@nestjs/schematics': 11.1.0(chokidar@4.0.3)(prettier@3.9.6)(typescript@5.9.3) ansis: 4.2.0 chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.2(@swc/core@1.15.43(@swc/helpers@0.5.23))(esbuild@0.28.1)(lightningcss@1.33.0)) + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) glob: 13.0.6 node-emoji: 1.11.0 ora: 5.4.1 tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 typescript: 5.9.3 - webpack: 5.106.2(@swc/core@1.15.43(@swc/helpers@0.5.23))(esbuild@0.28.1)(lightningcss@1.33.0) + webpack: 5.106.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) webpack-node-externals: 3.0.0 optionalDependencies: - '@swc/core': 1.15.43(@swc/helpers@0.5.23) + '@swc/core': 1.15.46(@swc/helpers@0.5.23) transitivePeerDependencies: - '@minify-html/node' - '@swc/css' @@ -16605,9 +16479,9 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1)': dependencies: - file-type: 21.3.4 + file-type: 21.3.4(supports-color@8.1.1) iterare: 1.2.1 load-esm: 1.0.3 reflect-metadata: 0.2.2 @@ -16617,9 +16491,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/core@11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) fast-safe-stringify: 2.1.1 iterare: 1.2.1 path-to-regexp: 8.4.2 @@ -16628,45 +16502,45 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/platform-express': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28) - '@nestjs/websockets': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@nestjs/platform-socket.io@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(supports-color@8.1.1) + '@nestjs/websockets': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(@nestjs/platform-socket.io@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)': + '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) reflect-metadata: 0.2.2 - '@nestjs/platform-express@11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)': + '@nestjs/platform-express@11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(supports-color@8.1.1)': dependencies: - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) + '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) cors: 2.8.6 - express: 5.2.1 + express: 5.2.1(supports-color@8.1.1) multer: 2.2.0 path-to-regexp: 8.4.2 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@nestjs/platform-socket.io@11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.28)(rxjs@7.8.2)': + '@nestjs/platform-socket.io@11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/websockets@11.1.28)(rxjs@7.8.2)(supports-color@8.1.1)': dependencies: - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/websockets': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@nestjs/platform-socket.io@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) + '@nestjs/websockets': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(@nestjs/platform-socket.io@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) rxjs: 7.8.2 - socket.io: 4.8.3 + socket.io: 4.8.3(supports-color@8.1.1) tslib: 2.8.1 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - '@nestjs/schedule@6.1.3(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)': + '@nestjs/schedule@6.1.3(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)': dependencies: - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) + '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) cron: 4.4.0 - '@nestjs/schematics@11.1.0(@typescript/typescript6@6.0.2)(chokidar@4.0.3)(prettier@3.9.5)': + '@nestjs/schematics@11.1.0(@typescript/typescript6@6.0.2)(chokidar@4.0.3)(prettier@3.9.6)': dependencies: '@angular-devkit/core': 19.2.24(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) @@ -16675,11 +16549,11 @@ snapshots: pluralize: 8.0.0 typescript: '@typescript/typescript6@6.0.2' optionalDependencies: - prettier: 3.9.5 + prettier: 3.9.6 transitivePeerDependencies: - chokidar - '@nestjs/schematics@11.1.0(chokidar@4.0.3)(prettier@3.9.5)(typescript@5.9.3)': + '@nestjs/schematics@11.1.0(chokidar@4.0.3)(prettier@3.9.6)(typescript@5.9.3)': dependencies: '@angular-devkit/core': 19.2.24(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) @@ -16688,42 +16562,42 @@ snapshots: pluralize: 8.0.0 typescript: 5.9.3 optionalDependencies: - prettier: 3.9.5 + prettier: 3.9.6 transitivePeerDependencies: - chokidar - '@nestjs/swagger@11.4.5(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@typescript/typescript6@6.0.2)(reflect-metadata@0.2.2)': + '@nestjs/swagger@11.4.6(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(@typescript/typescript6@6.0.2)(reflect-metadata@0.2.2)': dependencies: '@microsoft/tsdoc': 0.16.0 - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/mapped-types': 2.1.1(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) - js-yaml: 4.3.0 + '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) + '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.1(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(reflect-metadata@0.2.2) + js-yaml: 5.2.1 lodash: 4.18.1 path-to-regexp: 8.4.2 reflect-metadata: 0.2.2 swagger-ui-dist: 5.32.8 typescript: '@typescript/typescript6@6.0.2' - '@nestjs/testing@11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@nestjs/platform-express@11.1.28)': + '@nestjs/testing@11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(@nestjs/platform-express@11.1.28)': dependencies: - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) + '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-express': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28) + '@nestjs/platform-express': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(supports-color@8.1.1) - '@nestjs/websockets@11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@nestjs/platform-socket.io@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/websockets@11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(@nestjs/platform-socket.io@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) + '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) iterare: 1.2.1 object-hash: 3.0.0 reflect-metadata: 0.2.2 rxjs: 7.8.2 tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-socket.io': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.28)(rxjs@7.8.2) + '@nestjs/platform-socket.io': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/websockets@11.1.28)(rxjs@7.8.2)(supports-color@8.1.1) '@noble/hashes@1.4.0': {} @@ -16745,125 +16619,108 @@ snapshots: '@oazapfts/runtime@1.2.0': {} - '@opentelemetry/api-logs@0.220.0': + '@opentelemetry/api-logs@0.221.0': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/api@1.9.1': {} - '@opentelemetry/configuration@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/configuration@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) yaml: 2.9.0 - '@opentelemetry/context-async-hooks@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/context-async-hooks@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/semantic-conventions': 1.43.0 - '@opentelemetry/exporter-logs-otlp-grpc@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-logs-otlp-grpc@0.221.0(@opentelemetry/api@1.9.1)': dependencies: - '@grpc/grpc-js': 1.14.4 '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-grpc-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.221.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-logs-otlp-http@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-logs-otlp-http@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.220.0 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.221.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-logs-otlp-proto@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-logs-otlp-proto@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.221.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-grpc@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-metrics-otlp-grpc@0.221.0(@opentelemetry/api@1.9.1)': dependencies: - '@grpc/grpc-js': 1.14.4 '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-http': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-grpc-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-http': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-http@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-metrics-otlp-http@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-proto@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-metrics-otlp-proto@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-http': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-http': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-prometheus@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-prometheus@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 - '@opentelemetry/exporter-trace-otlp-grpc@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-trace-otlp-grpc@0.221.0(@opentelemetry/api@1.9.1)': dependencies: - '@grpc/grpc-js': 1.14.4 '@opentelemetry/api': 1.9.1 - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-grpc-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-trace-otlp-http@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-trace-otlp-http@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-trace-otlp-proto@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-trace-otlp-proto@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-zipkin@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/exporter-zipkin@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 '@opentelemetry/host-metrics@0.38.3(@opentelemetry/api@1.9.1)': @@ -16871,38 +16728,38 @@ snapshots: '@opentelemetry/api': 1.9.1 systeminformation: 5.31.17 - '@opentelemetry/instrumentation-http@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/instrumentation-http@0.221.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.221.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) '@opentelemetry/semantic-conventions': 1.43.0 forwarded-parse: 2.1.2 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-ioredis@0.68.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/instrumentation-ioredis@0.69.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.221.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) '@opentelemetry/redis-common': 0.38.3 '@opentelemetry/semantic-conventions': 1.43.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-nestjs-core@0.66.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/instrumentation-nestjs-core@0.67.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.221.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) '@opentelemetry/semantic-conventions': 1.43.0 transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation-pg@0.72.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/instrumentation-pg@0.73.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.221.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) '@opentelemetry/semantic-conventions': 1.43.0 '@opentelemetry/sql-common': 0.42.0(@opentelemetry/api@1.9.1) '@types/pg': 8.15.6 @@ -16910,124 +16767,124 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/instrumentation@0.221.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.220.0 + '@opentelemetry/api-logs': 0.221.0 import-in-the-middle: 3.0.2 - require-in-the-middle: 8.0.1 + require-in-the-middle: 8.0.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@opentelemetry/otlp-exporter-base@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/otlp-exporter-base@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-grpc-exporter-base@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/otlp-grpc-exporter-base@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@grpc/grpc-js': 1.14.4 '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer': 0.220.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-transformer': 0.221.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-transformer@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/otlp-transformer@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.220.0 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/api-logs': 0.221.0 + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/propagator-b3@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/propagator-b3@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/propagator-jaeger@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/propagator-jaeger@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/redis-common@0.38.3': {} - '@opentelemetry/resources@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 - '@opentelemetry/sdk-logs@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-logs@0.221.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.220.0 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/api-logs': 0.221.0 + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 - '@opentelemetry/sdk-metrics@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-node@0.220.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-node@0.221.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.220.0 - '@opentelemetry/configuration': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/context-async-hooks': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-logs-otlp-grpc': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-logs-otlp-http': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-logs-otlp-proto': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-grpc': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-http': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-metrics-otlp-proto': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-prometheus': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-trace-otlp-grpc': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-trace-otlp-http': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-trace-otlp-proto': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/exporter-zipkin': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/otlp-grpc-exporter-base': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/propagator-b3': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/propagator-jaeger': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-logs': 0.220.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-metrics': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-node': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/api-logs': 0.221.0 + '@opentelemetry/configuration': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/context-async-hooks': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-logs-otlp-grpc': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-logs-otlp-http': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-logs-otlp-proto': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-http': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-metrics-otlp-proto': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-prometheus': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-grpc': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-http': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-trace-otlp-proto': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/exporter-zipkin': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.221.0(@opentelemetry/api@1.9.1)(supports-color@8.1.1) + '@opentelemetry/otlp-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/otlp-grpc-exporter-base': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/propagator-b3': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/propagator-jaeger': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-logs': 0.221.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-metrics': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-node': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 transitivePeerDependencies: - supports-color - '@opentelemetry/sdk-trace-base@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 - '@opentelemetry/sdk-trace-node@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-trace-node@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/context-async-hooks': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/context-async-hooks': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace@2.9.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/sdk-trace@2.10.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.43.0 '@opentelemetry/semantic-conventions@1.43.0': {} @@ -17035,7 +16892,7 @@ snapshots: '@opentelemetry/sql-common@0.42.0(@opentelemetry/api@1.9.1)': dependencies: '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) '@oxc-project/types@0.139.0': {} @@ -17198,32 +17055,32 @@ snapshots: tslib: 2.8.1 tsyringe: 4.10.0 - '@photo-sphere-viewer/core@5.14.3': + '@photo-sphere-viewer/core@5.15.0': dependencies: - three: 0.184.0 - - '@photo-sphere-viewer/equirectangular-video-adapter@5.14.3(@photo-sphere-viewer/core@5.14.3)(@photo-sphere-viewer/video-plugin@5.14.3(@photo-sphere-viewer/core@5.14.3))': - dependencies: - '@photo-sphere-viewer/core': 5.14.3 - '@photo-sphere-viewer/video-plugin': 5.14.3(@photo-sphere-viewer/core@5.14.3) three: 0.185.1 - '@photo-sphere-viewer/markers-plugin@5.14.3(@photo-sphere-viewer/core@5.14.3)': + '@photo-sphere-viewer/equirectangular-video-adapter@5.15.0(@photo-sphere-viewer/core@5.15.0)(@photo-sphere-viewer/video-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0))': dependencies: - '@photo-sphere-viewer/core': 5.14.3 + '@photo-sphere-viewer/core': 5.15.0 + '@photo-sphere-viewer/video-plugin': 5.15.0(@photo-sphere-viewer/core@5.15.0) + three: 0.185.1 - '@photo-sphere-viewer/resolution-plugin@5.14.3(@photo-sphere-viewer/core@5.14.3)(@photo-sphere-viewer/settings-plugin@5.14.3(@photo-sphere-viewer/core@5.14.3))': + '@photo-sphere-viewer/markers-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0)': dependencies: - '@photo-sphere-viewer/core': 5.14.3 - '@photo-sphere-viewer/settings-plugin': 5.14.3(@photo-sphere-viewer/core@5.14.3) + '@photo-sphere-viewer/core': 5.15.0 - '@photo-sphere-viewer/settings-plugin@5.14.3(@photo-sphere-viewer/core@5.14.3)': + '@photo-sphere-viewer/resolution-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0)(@photo-sphere-viewer/settings-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0))': dependencies: - '@photo-sphere-viewer/core': 5.14.3 + '@photo-sphere-viewer/core': 5.15.0 + '@photo-sphere-viewer/settings-plugin': 5.15.0(@photo-sphere-viewer/core@5.15.0) - '@photo-sphere-viewer/video-plugin@5.14.3(@photo-sphere-viewer/core@5.14.3)': + '@photo-sphere-viewer/settings-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0)': dependencies: - '@photo-sphere-viewer/core': 5.14.3 + '@photo-sphere-viewer/core': 5.15.0 + + '@photo-sphere-viewer/video-plugin@5.15.0(@photo-sphere-viewer/core@5.15.0)': + dependencies: + '@photo-sphere-viewer/core': 5.15.0 three: 0.185.1 '@photostructure/tz-lookup@11.5.0': {} @@ -17233,9 +17090,9 @@ snapshots: '@pkgr/core@0.3.6': {} - '@playwright/test@1.61.1': + '@playwright/test@1.62.0': dependencies: - playwright: 1.61.1 + playwright: 1.62.0 '@pnpm/config.env-replace@1.1.0': {} @@ -17271,144 +17128,144 @@ snapshots: '@protobufjs/utf8@1.1.2': {} - '@react-email/body@0.3.0(react@19.2.7)': + '@react-email/body@0.3.0(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/button@0.2.1(react@19.2.7)': + '@react-email/button@0.2.1(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/code-block@0.2.1(react@19.2.7)': + '@react-email/code-block@0.2.1(react@19.2.8)': dependencies: prismjs: 1.30.0 - react: 19.2.7 + react: 19.2.8 - '@react-email/code-inline@0.0.6(react@19.2.7)': + '@react-email/code-inline@0.0.6(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/column@0.0.14(react@19.2.7)': + '@react-email/column@0.0.14(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/components@1.0.12(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@react-email/components@1.0.12(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@react-email/body': 0.3.0(react@19.2.7) - '@react-email/button': 0.2.1(react@19.2.7) - '@react-email/code-block': 0.2.1(react@19.2.7) - '@react-email/code-inline': 0.0.6(react@19.2.7) - '@react-email/column': 0.0.14(react@19.2.7) - '@react-email/container': 0.0.16(react@19.2.7) - '@react-email/font': 0.0.10(react@19.2.7) - '@react-email/head': 0.0.13(react@19.2.7) - '@react-email/heading': 0.0.16(react@19.2.7) - '@react-email/hr': 0.0.12(react@19.2.7) - '@react-email/html': 0.0.12(react@19.2.7) - '@react-email/img': 0.0.12(react@19.2.7) - '@react-email/link': 0.0.13(react@19.2.7) - '@react-email/markdown': 0.0.18(react@19.2.7) - '@react-email/preview': 0.0.14(react@19.2.7) - '@react-email/render': 2.0.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@react-email/row': 0.0.13(react@19.2.7) - '@react-email/section': 0.0.17(react@19.2.7) - '@react-email/tailwind': 2.0.7(@react-email/body@0.3.0(react@19.2.7))(@react-email/button@0.2.1(react@19.2.7))(@react-email/code-block@0.2.1(react@19.2.7))(@react-email/code-inline@0.0.6(react@19.2.7))(@react-email/container@0.0.16(react@19.2.7))(@react-email/heading@0.0.16(react@19.2.7))(@react-email/hr@0.0.12(react@19.2.7))(@react-email/img@0.0.12(react@19.2.7))(@react-email/link@0.0.13(react@19.2.7))(@react-email/preview@0.0.14(react@19.2.7))(@react-email/text@0.1.6(react@19.2.7))(react@19.2.7) - '@react-email/text': 0.1.6(react@19.2.7) - react: 19.2.7 + '@react-email/body': 0.3.0(react@19.2.8) + '@react-email/button': 0.2.1(react@19.2.8) + '@react-email/code-block': 0.2.1(react@19.2.8) + '@react-email/code-inline': 0.0.6(react@19.2.8) + '@react-email/column': 0.0.14(react@19.2.8) + '@react-email/container': 0.0.16(react@19.2.8) + '@react-email/font': 0.0.10(react@19.2.8) + '@react-email/head': 0.0.13(react@19.2.8) + '@react-email/heading': 0.0.16(react@19.2.8) + '@react-email/hr': 0.0.12(react@19.2.8) + '@react-email/html': 0.0.12(react@19.2.8) + '@react-email/img': 0.0.12(react@19.2.8) + '@react-email/link': 0.0.13(react@19.2.8) + '@react-email/markdown': 0.0.18(react@19.2.8) + '@react-email/preview': 0.0.14(react@19.2.8) + '@react-email/render': 2.0.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@react-email/row': 0.0.13(react@19.2.8) + '@react-email/section': 0.0.17(react@19.2.8) + '@react-email/tailwind': 2.0.7(@react-email/body@0.3.0(react@19.2.8))(@react-email/button@0.2.1(react@19.2.8))(@react-email/code-block@0.2.1(react@19.2.8))(@react-email/code-inline@0.0.6(react@19.2.8))(@react-email/container@0.0.16(react@19.2.8))(@react-email/heading@0.0.16(react@19.2.8))(@react-email/hr@0.0.12(react@19.2.8))(@react-email/img@0.0.12(react@19.2.8))(@react-email/link@0.0.13(react@19.2.8))(@react-email/preview@0.0.14(react@19.2.8))(@react-email/text@0.1.6(react@19.2.8))(react@19.2.8) + '@react-email/text': 0.1.6(react@19.2.8) + react: 19.2.8 transitivePeerDependencies: - react-dom - '@react-email/container@0.0.16(react@19.2.7)': + '@react-email/container@0.0.16(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/font@0.0.10(react@19.2.7)': + '@react-email/font@0.0.10(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/head@0.0.13(react@19.2.7)': + '@react-email/head@0.0.13(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/heading@0.0.16(react@19.2.7)': + '@react-email/heading@0.0.16(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/hr@0.0.12(react@19.2.7)': + '@react-email/hr@0.0.12(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/html@0.0.12(react@19.2.7)': + '@react-email/html@0.0.12(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/img@0.0.12(react@19.2.7)': + '@react-email/img@0.0.12(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/link@0.0.13(react@19.2.7)': + '@react-email/link@0.0.13(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/markdown@0.0.18(react@19.2.7)': + '@react-email/markdown@0.0.18(react@19.2.8)': dependencies: marked: 15.0.12 - react: 19.2.7 + react: 19.2.8 - '@react-email/preview@0.0.14(react@19.2.7)': + '@react-email/preview@0.0.14(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/render@2.0.6(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@react-email/render@2.0.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: html-to-text: 9.0.5 - prettier: 3.9.5 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + prettier: 3.9.6 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) - '@react-email/render@2.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@react-email/render@2.1.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: entities: 4.5.0 html-to-text: 9.0.5 html5parser: 3.0.0 - prettier: 3.9.5 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + prettier: 3.9.6 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) - '@react-email/row@0.0.13(react@19.2.7)': + '@react-email/row@0.0.13(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/section@0.0.17(react@19.2.7)': + '@react-email/section@0.0.17(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@react-email/tailwind@2.0.7(@react-email/body@0.3.0(react@19.2.7))(@react-email/button@0.2.1(react@19.2.7))(@react-email/code-block@0.2.1(react@19.2.7))(@react-email/code-inline@0.0.6(react@19.2.7))(@react-email/container@0.0.16(react@19.2.7))(@react-email/heading@0.0.16(react@19.2.7))(@react-email/hr@0.0.12(react@19.2.7))(@react-email/img@0.0.12(react@19.2.7))(@react-email/link@0.0.13(react@19.2.7))(@react-email/preview@0.0.14(react@19.2.7))(@react-email/text@0.1.6(react@19.2.7))(react@19.2.7)': + '@react-email/tailwind@2.0.7(@react-email/body@0.3.0(react@19.2.8))(@react-email/button@0.2.1(react@19.2.8))(@react-email/code-block@0.2.1(react@19.2.8))(@react-email/code-inline@0.0.6(react@19.2.8))(@react-email/container@0.0.16(react@19.2.8))(@react-email/heading@0.0.16(react@19.2.8))(@react-email/hr@0.0.12(react@19.2.8))(@react-email/img@0.0.12(react@19.2.8))(@react-email/link@0.0.13(react@19.2.8))(@react-email/preview@0.0.14(react@19.2.8))(@react-email/text@0.1.6(react@19.2.8))(react@19.2.8)': dependencies: - '@react-email/text': 0.1.6(react@19.2.7) - react: 19.2.7 - tailwindcss: 4.3.2 + '@react-email/text': 0.1.6(react@19.2.8) + react: 19.2.8 + tailwindcss: 4.3.3 optionalDependencies: - '@react-email/body': 0.3.0(react@19.2.7) - '@react-email/button': 0.2.1(react@19.2.7) - '@react-email/code-block': 0.2.1(react@19.2.7) - '@react-email/code-inline': 0.0.6(react@19.2.7) - '@react-email/container': 0.0.16(react@19.2.7) - '@react-email/heading': 0.0.16(react@19.2.7) - '@react-email/hr': 0.0.12(react@19.2.7) - '@react-email/img': 0.0.12(react@19.2.7) - '@react-email/link': 0.0.13(react@19.2.7) - '@react-email/preview': 0.0.14(react@19.2.7) + '@react-email/body': 0.3.0(react@19.2.8) + '@react-email/button': 0.2.1(react@19.2.8) + '@react-email/code-block': 0.2.1(react@19.2.8) + '@react-email/code-inline': 0.0.6(react@19.2.8) + '@react-email/container': 0.0.16(react@19.2.8) + '@react-email/heading': 0.0.16(react@19.2.8) + '@react-email/hr': 0.0.12(react@19.2.8) + '@react-email/img': 0.0.12(react@19.2.8) + '@react-email/link': 0.0.13(react@19.2.8) + '@react-email/preview': 0.0.14(react@19.2.8) - '@react-email/text@0.1.6(react@19.2.7)': + '@react-email/text@0.1.6(react@19.2.8)': dependencies: - react: 19.2.7 + react: 19.2.8 - '@replit/codemirror-indentation-markers@6.5.3(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.1)': + '@replit/codemirror-indentation-markers@6.5.3(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.7)': dependencies: - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.7 '@rolldown/binding-android-arm64@1.1.5': optional: true @@ -17565,13 +17422,13 @@ snapshots: '@sindresorhus/is@5.6.0': {} - '@slorber/react-helmet-async@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + '@slorber/react-helmet-async@1.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@babel/runtime': 7.29.7 invariant: 2.2.4 prop-types: 15.8.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) react-fast-compare: 3.2.2 shallowequal: 1.1.0 @@ -17583,11 +17440,11 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} - '@socket.io/redis-adapter@8.3.0(socket.io-adapter@2.5.7)': + '@socket.io/redis-adapter@8.3.0(socket.io-adapter@2.5.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) notepack.io: 3.0.1 - socket.io-adapter: 2.5.7 + socket.io-adapter: 2.5.7(supports-color@8.1.1) uid2: 1.0.0 transitivePeerDependencies: - supports-color @@ -17596,107 +17453,107 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@sveltejs/acorn-typescript@1.0.11(acorn@8.17.0)': + '@sveltejs/acorn-typescript@1.0.11(acorn@8.18.0)': dependencies: - acorn: 8.17.0 + acorn: 8.18.0 - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))': dependencies: - '@sveltejs/kit': 2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + '@sveltejs/kit': 2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) - '@sveltejs/enhanced-img@0.11.0(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(rollup@4.62.0)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': + '@sveltejs/enhanced-img@0.11.0(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(rollup@4.62.0)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + '@sveltejs/vite-plugin-svelte': 7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) magic-string: 0.30.21 sharp: 0.34.5 - svelte: 5.56.5(@typescript-eslint/types@8.64.0) - svelte-parse-markup: 0.1.5(svelte@5.56.5(@typescript-eslint/types@8.64.0)) - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte-parse-markup: 0.1.5(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) vite-imagetools: 9.0.3(rollup@4.62.0) zimmerframe: 1.1.4 transitivePeerDependencies: - rollup - '@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': + '@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': dependencies: '@standard-schema/spec': 1.1.0 - '@sveltejs/acorn-typescript': 1.0.11(acorn@8.17.0) - '@sveltejs/vite-plugin-svelte': 7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + '@sveltejs/acorn-typescript': 1.0.11(acorn@8.18.0) + '@sveltejs/vite-plugin-svelte': 7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) '@types/cookie': 0.6.0 - acorn: 8.17.0 + acorn: 8.18.0 cookie: 0.6.0 - devalue: 5.8.1 + devalue: 5.8.2 esm-env: 1.2.2 kleur: 4.1.5 magic-string: 0.30.21 mrmime: 2.0.1 set-cookie-parser: 3.1.2 sirv: 3.0.2 - svelte: 5.56.5(@typescript-eslint/types@8.64.0) - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) optionalDependencies: '@opentelemetry/api': 1.9.1 typescript: '@typescript/typescript6@6.0.2' '@sveltejs/load-config@0.2.0': {} - '@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': + '@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': dependencies: deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.3 - svelte: 5.56.5(@typescript-eslint/types@8.64.0) - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) - vitefu: 1.1.3(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vitefu: 1.1.3(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) - '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.29.7)': + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.29.7)': + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.29.7)': + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.29.7)': + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.29.7)': + '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.29.7)': + '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.29.7)': + '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.29.7)': + '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) - '@svgr/babel-preset@8.1.0(@babel/core@7.29.7)': + '@svgr/babel-preset@8.1.0(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 - '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.29.7) - '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.29.7) - '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.29.7) - '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.29.7) - '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.29.7) - '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.29.7) - '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.29.7) - '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.29.7(supports-color@8.1.1)) + '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.29.7(supports-color@8.1.1)) + '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.29.7(supports-color@8.1.1)) + '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.29.7(supports-color@8.1.1)) + '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.29.7(supports-color@8.1.1)) + '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.29.7(supports-color@8.1.1)) + '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.29.7(supports-color@8.1.1)) + '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.29.7(supports-color@8.1.1)) - '@svgr/core@8.1.0(@typescript/typescript6@6.0.2)': + '@svgr/core@8.1.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@svgr/babel-preset': 8.1.0(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@svgr/babel-preset': 8.1.0(@babel/core@7.29.7(supports-color@8.1.1)) camelcase: 6.3.0 cosmiconfig: 8.3.6(@typescript/typescript6@6.0.2) snake-case: 3.0.4 @@ -17709,92 +17566,92 @@ snapshots: '@babel/types': 7.29.7 entities: 4.5.0 - '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(@typescript/typescript6@6.0.2))': + '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@svgr/babel-preset': 8.1.0(@babel/core@7.29.7) - '@svgr/core': 8.1.0(@typescript/typescript6@6.0.2) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@svgr/babel-preset': 8.1.0(@babel/core@7.29.7(supports-color@8.1.1)) + '@svgr/core': 8.1.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) '@svgr/hast-util-to-babel-ast': 8.0.0 svg-parser: 2.0.4 transitivePeerDependencies: - supports-color - '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(@typescript/typescript6@6.0.2))(@typescript/typescript6@6.0.2)': + '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1))(@typescript/typescript6@6.0.2)': dependencies: - '@svgr/core': 8.1.0(@typescript/typescript6@6.0.2) + '@svgr/core': 8.1.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) cosmiconfig: 8.3.6(@typescript/typescript6@6.0.2) deepmerge: 4.3.1 svgo: 3.3.4 transitivePeerDependencies: - typescript - '@svgr/webpack@8.1.0(@typescript/typescript6@6.0.2)': + '@svgr/webpack@8.1.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/plugin-transform-react-constant-elements': 7.29.7(@babel/core@7.29.7) - '@babel/preset-env': 7.29.7(@babel/core@7.29.7) - '@babel/preset-react': 7.29.7(@babel/core@7.29.7) - '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7) - '@svgr/core': 8.1.0(@typescript/typescript6@6.0.2) - '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(@typescript/typescript6@6.0.2)) - '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(@typescript/typescript6@6.0.2))(@typescript/typescript6@6.0.2) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/plugin-transform-react-constant-elements': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/preset-env': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/preset-react': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@svgr/core': 8.1.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1))(supports-color@8.1.1) + '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1))(@typescript/typescript6@6.0.2) transitivePeerDependencies: - supports-color - typescript - '@swc/core-darwin-arm64@1.15.43': + '@swc/core-darwin-arm64@1.15.46': optional: true - '@swc/core-darwin-x64@1.15.43': + '@swc/core-darwin-x64@1.15.46': optional: true - '@swc/core-linux-arm-gnueabihf@1.15.43': + '@swc/core-linux-arm-gnueabihf@1.15.46': optional: true - '@swc/core-linux-arm64-gnu@1.15.43': + '@swc/core-linux-arm64-gnu@1.15.46': optional: true - '@swc/core-linux-arm64-musl@1.15.43': + '@swc/core-linux-arm64-musl@1.15.46': optional: true - '@swc/core-linux-ppc64-gnu@1.15.43': + '@swc/core-linux-ppc64-gnu@1.15.46': optional: true - '@swc/core-linux-s390x-gnu@1.15.43': + '@swc/core-linux-s390x-gnu@1.15.46': optional: true - '@swc/core-linux-x64-gnu@1.15.43': + '@swc/core-linux-x64-gnu@1.15.46': optional: true - '@swc/core-linux-x64-musl@1.15.43': + '@swc/core-linux-x64-musl@1.15.46': optional: true - '@swc/core-win32-arm64-msvc@1.15.43': + '@swc/core-win32-arm64-msvc@1.15.46': optional: true - '@swc/core-win32-ia32-msvc@1.15.43': + '@swc/core-win32-ia32-msvc@1.15.46': optional: true - '@swc/core-win32-x64-msvc@1.15.43': + '@swc/core-win32-x64-msvc@1.15.46': optional: true - '@swc/core@1.15.43(@swc/helpers@0.5.23)': + '@swc/core@1.15.46(@swc/helpers@0.5.23)': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.27 optionalDependencies: - '@swc/core-darwin-arm64': 1.15.43 - '@swc/core-darwin-x64': 1.15.43 - '@swc/core-linux-arm-gnueabihf': 1.15.43 - '@swc/core-linux-arm64-gnu': 1.15.43 - '@swc/core-linux-arm64-musl': 1.15.43 - '@swc/core-linux-ppc64-gnu': 1.15.43 - '@swc/core-linux-s390x-gnu': 1.15.43 - '@swc/core-linux-x64-gnu': 1.15.43 - '@swc/core-linux-x64-musl': 1.15.43 - '@swc/core-win32-arm64-msvc': 1.15.43 - '@swc/core-win32-ia32-msvc': 1.15.43 - '@swc/core-win32-x64-msvc': 1.15.43 + '@swc/core-darwin-arm64': 1.15.46 + '@swc/core-darwin-x64': 1.15.46 + '@swc/core-linux-arm-gnueabihf': 1.15.46 + '@swc/core-linux-arm64-gnu': 1.15.46 + '@swc/core-linux-arm64-musl': 1.15.46 + '@swc/core-linux-ppc64-gnu': 1.15.46 + '@swc/core-linux-s390x-gnu': 1.15.46 + '@swc/core-linux-x64-gnu': 1.15.46 + '@swc/core-linux-x64-musl': 1.15.46 + '@swc/core-win32-arm64-msvc': 1.15.46 + '@swc/core-win32-ia32-msvc': 1.15.46 + '@swc/core-win32-x64-msvc': 1.15.46 '@swc/helpers': 0.5.23 '@swc/counter@0.1.3': {} @@ -17811,73 +17668,73 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@tailwindcss/node@4.3.2': + '@tailwindcss/node@4.3.3': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.21.6 + enhanced-resolve: 5.24.4 jiti: 2.7.0 lightningcss: 1.32.0 magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.3.2 + tailwindcss: 4.3.3 - '@tailwindcss/oxide-android-arm64@4.3.2': + '@tailwindcss/oxide-android-arm64@4.3.3': optional: true - '@tailwindcss/oxide-darwin-arm64@4.3.2': + '@tailwindcss/oxide-darwin-arm64@4.3.3': optional: true - '@tailwindcss/oxide-darwin-x64@4.3.2': + '@tailwindcss/oxide-darwin-x64@4.3.3': optional: true - '@tailwindcss/oxide-freebsd-x64@4.3.2': + '@tailwindcss/oxide-freebsd-x64@4.3.3': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.3.2': + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.3.2': + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.3.2': + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.3.2': + '@tailwindcss/oxide-linux-x64-musl@4.3.3': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.3.2': + '@tailwindcss/oxide-wasm32-wasi@4.3.3': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.3.2': + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.3.2': + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': optional: true - '@tailwindcss/oxide@4.3.2': + '@tailwindcss/oxide@4.3.3': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.3.2 - '@tailwindcss/oxide-darwin-arm64': 4.3.2 - '@tailwindcss/oxide-darwin-x64': 4.3.2 - '@tailwindcss/oxide-freebsd-x64': 4.3.2 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.2 - '@tailwindcss/oxide-linux-arm64-gnu': 4.3.2 - '@tailwindcss/oxide-linux-arm64-musl': 4.3.2 - '@tailwindcss/oxide-linux-x64-gnu': 4.3.2 - '@tailwindcss/oxide-linux-x64-musl': 4.3.2 - '@tailwindcss/oxide-wasm32-wasi': 4.3.2 - '@tailwindcss/oxide-win32-arm64-msvc': 4.3.2 - '@tailwindcss/oxide-win32-x64-msvc': 4.3.2 + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - '@tailwindcss/vite@4.3.2(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': + '@tailwindcss/vite@4.3.3(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': dependencies: - '@tailwindcss/node': 4.3.2 - '@tailwindcss/oxide': 4.3.2 - tailwindcss: 4.3.2 - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + tailwindcss: 4.3.3 + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) '@testing-library/dom@10.4.1': dependencies: @@ -17899,46 +17756,46 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/svelte-core@1.1.3(svelte@5.56.5(@typescript-eslint/types@8.64.0))': + '@testing-library/svelte-core@1.1.3(svelte@5.56.8(@typescript-eslint/types@8.65.0))': dependencies: - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) - '@testing-library/svelte@5.4.2(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))(vitest@4.1.10)': + '@testing-library/svelte@5.4.2(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))(vitest@4.1.10)': dependencies: '@testing-library/dom': 10.4.1 - '@testing-library/svelte-core': 1.1.3(svelte@5.56.5(@typescript-eslint/types@8.64.0)) - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + '@testing-library/svelte-core': 1.1.3(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) optionalDependencies: - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6)(jsdom@26.1.0(canvas@3.2.3))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: '@testing-library/dom': 10.4.1 - '@tokenizer/inflate@0.4.1': + '@tokenizer/inflate@0.4.1(supports-color@8.1.1)': dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) token-types: 6.1.2 transitivePeerDependencies: - supports-color '@tokenizer/token@0.3.0': {} - '@trivago/prettier-plugin-sort-imports@6.0.2(prettier-plugin-svelte@4.1.1(prettier@3.9.5)(svelte@5.56.5(@typescript-eslint/types@8.64.0)))(prettier@3.9.5)(svelte@5.56.5(@typescript-eslint/types@8.64.0))': + '@trivago/prettier-plugin-sort-imports@6.0.2(prettier-plugin-svelte@4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.65.0)))(prettier@3.9.6)(supports-color@8.1.1)(svelte@5.56.8(@typescript-eslint/types@8.65.0))': dependencies: '@babel/generator': 7.29.7 '@babel/parser': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.7(supports-color@8.1.1) '@babel/types': 7.29.7 javascript-natural-sort: 0.7.1 lodash-es: 4.18.1 minimatch: 9.0.9 parse-imports-exports: 0.2.4 - prettier: 3.9.5 + prettier: 3.9.6 optionalDependencies: - prettier-plugin-svelte: 4.1.1(prettier@3.9.5)(svelte@5.56.5(@typescript-eslint/types@8.64.0)) - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + prettier-plugin-svelte: 4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) transitivePeerDependencies: - supports-color @@ -18510,15 +18367,15 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@8.64.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)))(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0))': + '@typescript-eslint/eslint-plugin@8.65.0(@typescript-eslint/parser@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1))(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) - '@typescript-eslint/scope-manager': 8.64.0 - '@typescript-eslint/type-utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) - '@typescript-eslint/utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) - '@typescript-eslint/visitor-keys': 8.64.0 - eslint: 10.7.0(jiti@2.7.0) + '@typescript-eslint/parser': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/type-utils': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/utils': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/visitor-keys': 8.65.0 + eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) ignore: 7.0.6 natural-compare: 1.4.0 ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) @@ -18526,58 +18383,58 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0))': + '@typescript-eslint/parser@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@typescript-eslint/scope-manager': 8.64.0 - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/typescript-estree': 8.64.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/visitor-keys': 8.64.0 - debug: 4.4.3 - eslint: 10.7.0(jiti@2.7.0) + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@typescript-eslint/visitor-keys': 8.65.0 + debug: 4.4.3(supports-color@8.1.1) + eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.64.0(@typescript/typescript6@6.0.2)': + '@typescript-eslint/project-service@8.65.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.64.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/types': 8.64.0 - debug: 4.4.3 + '@typescript-eslint/tsconfig-utils': 8.65.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/types': 8.65.0 + debug: 4.4.3(supports-color@8.1.1) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.64.0': + '@typescript-eslint/scope-manager@8.65.0': dependencies: - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/visitor-keys': 8.64.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/visitor-keys': 8.65.0 - '@typescript-eslint/tsconfig-utils@8.64.0(@typescript/typescript6@6.0.2)': + '@typescript-eslint/tsconfig-utils@8.65.0(@typescript/typescript6@6.0.2)': dependencies: typescript: '@typescript/typescript6@6.0.2' - '@typescript-eslint/type-utils@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0))': + '@typescript-eslint/type-utils@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/typescript-estree': 8.64.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) - debug: 4.4.3 - eslint: 10.7.0(jiti@2.7.0) + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@typescript-eslint/utils': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) + eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.64.0': {} + '@typescript-eslint/types@8.65.0': {} - '@typescript-eslint/typescript-estree@8.64.0(@typescript/typescript6@6.0.2)': + '@typescript-eslint/typescript-estree@8.65.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1)': dependencies: - '@typescript-eslint/project-service': 8.64.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/tsconfig-utils': 8.64.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/visitor-keys': 8.64.0 - debug: 4.4.3 - minimatch: 10.2.5 + '@typescript-eslint/project-service': 8.65.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@typescript-eslint/tsconfig-utils': 8.65.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/visitor-keys': 8.65.0 + debug: 4.4.3(supports-color@8.1.1) + minimatch: 10.2.6 semver: 7.8.5 tinyglobby: 0.2.17 ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) @@ -18585,20 +18442,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0))': + '@typescript-eslint/utils@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)) - '@typescript-eslint/scope-manager': 8.64.0 - '@typescript-eslint/types': 8.64.0 - '@typescript-eslint/typescript-estree': 8.64.0(@typescript/typescript6@6.0.2) - eslint: 10.7.0(jiti@2.7.0) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) + '@typescript-eslint/scope-manager': 8.65.0 + '@typescript-eslint/types': 8.65.0 + '@typescript-eslint/typescript-estree': 8.65.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.64.0': + '@typescript-eslint/visitor-keys@8.65.0': dependencies: - '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/types': 8.65.0 eslint-visitor-keys: 5.0.1 '@typescript/typescript-aix-ppc64@7.0.2': @@ -18676,7 +18533,7 @@ snapshots: dependencies: valibot: 1.4.2(@typescript/typescript6@6.0.2) - '@vitest/coverage-v8@4.1.10(vitest@3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.10.6)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.33.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': + '@vitest/coverage-v8@4.1.10(vitest@3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.1.10 @@ -18688,7 +18545,7 @@ snapshots: obug: 2.1.3 std-env: 4.2.0 tinyrainbow: 3.1.0 - vitest: 3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.10.6)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.33.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vitest: 3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) '@vitest/coverage-v8@4.1.10(vitest@4.1.10)': dependencies: @@ -18702,7 +18559,7 @@ snapshots: obug: 2.1.3 std-env: 4.2.0 tinyrainbow: 3.1.0 - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6)(jsdom@26.1.0(canvas@3.2.3))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) '@vitest/expect@3.2.7': dependencies: @@ -18721,21 +18578,21 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': dependencies: '@vitest/spy': 3.2.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) - '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': + '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) '@vitest/pretty-format@3.2.7': dependencies: @@ -18871,10 +18728,10 @@ snapshots: dependencies: '@namnode/store': 0.1.0 - '@zoom-image/svelte@0.3.9(svelte@5.56.5(@typescript-eslint/types@8.64.0))': + '@zoom-image/svelte@0.3.9(svelte@5.56.8(@typescript-eslint/types@8.65.0))': dependencies: '@zoom-image/core': 0.42.0 - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) abbrev@1.1.1: {} @@ -18894,29 +18751,29 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 - acorn-import-attributes@1.9.5(acorn@8.17.0): + acorn-import-attributes@1.9.5(acorn@8.18.0): dependencies: - acorn: 8.17.0 + acorn: 8.18.0 - acorn-import-phases@1.0.4(acorn@8.17.0): + acorn-import-phases@1.0.4(acorn@8.18.0): dependencies: - acorn: 8.17.0 + acorn: 8.18.0 - acorn-jsx@5.3.2(acorn@8.17.0): + acorn-jsx@5.3.2(acorn@8.18.0): dependencies: - acorn: 8.17.0 + acorn: 8.18.0 acorn-walk@8.3.5: dependencies: - acorn: 8.17.0 + acorn: 8.18.0 - acorn@8.17.0: {} + acorn@8.18.0: {} address@2.0.3: {} - agent-base@6.0.2: + agent-base@6.0.2(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -18959,14 +18816,14 @@ snapshots: ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.3 + fast-uri: 3.1.4 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.3 + fast-uri: 3.1.4 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -19121,59 +18978,59 @@ snapshots: dependencies: immediate: 3.3.0 - autoprefixer@10.5.4(postcss@8.5.19): + autoprefixer@10.5.4(postcss@8.5.25): dependencies: browserslist: 4.28.6 caniuse-lite: 1.0.30001806 fraction.js: 5.3.4 picocolors: 1.1.1 - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 axobject-query@4.1.0: {} b4a@1.8.1: {} - babel-loader@9.2.1(@babel/core@7.29.7)(webpack@5.108.4(postcss@8.5.19)): + babel-loader@9.2.1(@babel/core@7.29.7(supports-color@8.1.1))(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) find-cache-dir: 4.0.0 schema-utils: 4.3.3 - webpack: 5.108.4(postcss@8.5.19) + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) babel-plugin-dynamic-import-node@2.3.3: dependencies: object.assign: 4.1.7 - babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.7): + babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1): dependencies: '@babel/compat-data': 7.29.7 - '@babel/core': 7.29.7 - '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) semver: 6.3.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.7): + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.7 - '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) core-js-compat: 3.49.0 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.14.2(@babel/core@7.29.7): + babel-plugin-polyfill-corejs3@0.14.2(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.7 - '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) core-js-compat: 3.49.0 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.7): + babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.7 - '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -19240,15 +19097,15 @@ snapshots: binary-extensions@2.3.0: {} - bits-ui@2.18.1(@internationalized/date@3.12.2)(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.5(@typescript-eslint/types@8.64.0)): + bits-ui@2.18.1(@internationalized/date@3.12.2)(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)): dependencies: - '@floating-ui/core': 1.7.5 - '@floating-ui/dom': 1.7.6 + '@floating-ui/core': 1.8.0 + '@floating-ui/dom': 1.8.0 '@internationalized/date': 3.12.2 esm-env: 1.2.2 - runed: 0.35.1(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.5(@typescript-eslint/types@8.64.0)) - svelte: 5.56.5(@typescript-eslint/types@8.64.0) - svelte-toolbelt: 0.10.6(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + runed: 0.35.1(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) tabbable: 6.5.0 transitivePeerDependencies: - '@sveltejs/kit' @@ -19259,11 +19116,11 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - body-parser@1.20.6: + body-parser@1.20.6(supports-color@8.1.1): dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) depd: 2.0.0 destroy: 1.2.0 http-errors: 2.0.1 @@ -19276,11 +19133,11 @@ snapshots: transitivePeerDependencies: - supports-color - body-parser@2.3.0: + body-parser@2.3.0(supports-color@8.1.1): dependencies: bytes: 3.1.2 content-type: 2.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) http-errors: 2.0.1 iconv-lite: 0.7.3 on-finished: 2.4.1 @@ -19328,7 +19185,7 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.7: + brace-expansion@5.0.8: dependencies: balanced-match: 4.0.4 @@ -19369,10 +19226,10 @@ snapshots: builtin-modules@5.2.0: {} - bullmq@5.80.5: + bullmq@5.81.2(supports-color@8.1.1): dependencies: cron-parser: 4.9.0 - ioredis: 5.11.1 + ioredis: 5.11.1(supports-color@8.1.1) msgpackr: 2.0.4 node-abort-controller: 3.1.1 semver: 7.8.5 @@ -19461,9 +19318,9 @@ snapshots: ccount@2.0.1: {} - ce-la-react@0.3.2(react@19.2.7): + ce-la-react@0.3.2(react@19.2.8): dependencies: - react: 19.2.7 + react: 19.2.8 chai@5.3.3: dependencies: @@ -19623,11 +19480,11 @@ snapshots: cluster-key-slot@1.1.1: {} - codemirror-wrapped-line-indent@1.0.9(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.1): + codemirror-wrapped-line-indent@1.0.9(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.7): dependencies: - '@codemirror/language': 6.12.3 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.7 collapse-white-space@2.1.0: {} @@ -19696,11 +19553,11 @@ snapshots: dependencies: mime-db: 1.54.0 - compression@1.8.1: + compression@1.8.1(supports-color@8.1.1): dependencies: bytes: 3.1.2 compressible: 2.0.18 - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) negotiator: 0.6.4 on-headers: 1.1.0 safe-buffer: 5.2.1 @@ -19792,7 +19649,7 @@ snapshots: copy-text-to-clipboard@3.2.2: {} - copy-webpack-plugin@11.0.0(webpack@5.108.4(postcss@8.5.19)): + copy-webpack-plugin@11.0.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): dependencies: fast-glob: 3.3.3 glob-parent: 6.0.2 @@ -19800,7 +19657,7 @@ snapshots: normalize-path: 3.0.0 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - webpack: 5.108.4(postcss@8.5.19) + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) core-js-compat@3.49.0: dependencies: @@ -19854,7 +19711,7 @@ snapshots: crc-32: 1.2.2 readable-stream: 4.7.0 - crelt@1.0.6: {} + crelt@1.0.7: {} cron-parser@4.9.0: dependencies: @@ -19875,50 +19732,53 @@ snapshots: dependencies: type-fest: 1.4.0 - css-blank-pseudo@7.0.1(postcss@8.5.19): + css-blank-pseudo@7.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 - css-declaration-sorter@7.4.0(postcss@8.5.19): + css-declaration-sorter@7.4.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - css-has-pseudo@7.0.3(postcss@8.5.19): + css-has-pseudo@7.0.3(postcss@8.5.25): dependencies: '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.4) - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 postcss-value-parser: 4.2.0 - css-loader@6.11.0(webpack@5.108.4(postcss@8.5.19)): + css-loader@6.11.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): dependencies: - icss-utils: 5.1.0(postcss@8.5.19) - postcss: 8.5.19 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.19) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.19) - postcss-modules-scope: 3.2.1(postcss@8.5.19) - postcss-modules-values: 4.0.0(postcss@8.5.19) + icss-utils: 5.1.0(postcss@8.5.25) + postcss: 8.5.25 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.25) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.25) + postcss-modules-scope: 3.2.1(postcss@8.5.25) + postcss-modules-values: 4.0.0(postcss@8.5.25) postcss-value-parser: 4.2.0 semver: 7.8.5 optionalDependencies: - webpack: 5.108.4(postcss@8.5.19) + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) - css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(webpack@5.108.4(postcss@8.5.19)): + css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.33.0)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): dependencies: '@jridgewell/trace-mapping': 0.3.31 - cssnano: 6.1.2(postcss@8.5.19) + cssnano: 6.1.2(postcss@8.5.25) jest-worker: 29.7.0 - postcss: 8.5.19 + postcss: 8.5.25 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - webpack: 5.108.4(postcss@8.5.19) + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) optionalDependencies: clean-css: 5.3.3 + csso: 5.0.5 + esbuild: 0.28.1 + lightningcss: 1.33.0 - css-prefers-color-scheme@10.0.0(postcss@8.5.19): + css-prefers-color-scheme@10.0.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 css-select@4.3.0: dependencies: @@ -19956,60 +19816,60 @@ snapshots: cssesc@3.0.0: {} - cssnano-preset-advanced@6.1.2(postcss@8.5.19): + cssnano-preset-advanced@6.1.2(postcss@8.5.25): dependencies: - autoprefixer: 10.5.4(postcss@8.5.19) + autoprefixer: 10.5.4(postcss@8.5.25) browserslist: 4.28.6 - cssnano-preset-default: 6.1.2(postcss@8.5.19) - postcss: 8.5.19 - postcss-discard-unused: 6.0.5(postcss@8.5.19) - postcss-merge-idents: 6.0.3(postcss@8.5.19) - postcss-reduce-idents: 6.0.3(postcss@8.5.19) - postcss-zindex: 6.0.2(postcss@8.5.19) + cssnano-preset-default: 6.1.2(postcss@8.5.25) + postcss: 8.5.25 + postcss-discard-unused: 6.0.5(postcss@8.5.25) + postcss-merge-idents: 6.0.3(postcss@8.5.25) + postcss-reduce-idents: 6.0.3(postcss@8.5.25) + postcss-zindex: 6.0.2(postcss@8.5.25) - cssnano-preset-default@6.1.2(postcss@8.5.19): + cssnano-preset-default@6.1.2(postcss@8.5.25): dependencies: browserslist: 4.28.6 - css-declaration-sorter: 7.4.0(postcss@8.5.19) - cssnano-utils: 4.0.2(postcss@8.5.19) - postcss: 8.5.19 - postcss-calc: 9.0.1(postcss@8.5.19) - postcss-colormin: 6.1.0(postcss@8.5.19) - postcss-convert-values: 6.1.0(postcss@8.5.19) - postcss-discard-comments: 6.0.2(postcss@8.5.19) - postcss-discard-duplicates: 6.0.3(postcss@8.5.19) - postcss-discard-empty: 6.0.3(postcss@8.5.19) - postcss-discard-overridden: 6.0.2(postcss@8.5.19) - postcss-merge-longhand: 6.0.5(postcss@8.5.19) - postcss-merge-rules: 6.1.1(postcss@8.5.19) - postcss-minify-font-values: 6.1.0(postcss@8.5.19) - postcss-minify-gradients: 6.0.3(postcss@8.5.19) - postcss-minify-params: 6.1.0(postcss@8.5.19) - postcss-minify-selectors: 6.0.4(postcss@8.5.19) - postcss-normalize-charset: 6.0.2(postcss@8.5.19) - postcss-normalize-display-values: 6.0.2(postcss@8.5.19) - postcss-normalize-positions: 6.0.2(postcss@8.5.19) - postcss-normalize-repeat-style: 6.0.2(postcss@8.5.19) - postcss-normalize-string: 6.0.2(postcss@8.5.19) - postcss-normalize-timing-functions: 6.0.2(postcss@8.5.19) - postcss-normalize-unicode: 6.1.0(postcss@8.5.19) - postcss-normalize-url: 6.0.2(postcss@8.5.19) - postcss-normalize-whitespace: 6.0.2(postcss@8.5.19) - postcss-ordered-values: 6.0.2(postcss@8.5.19) - postcss-reduce-initial: 6.1.0(postcss@8.5.19) - postcss-reduce-transforms: 6.0.2(postcss@8.5.19) - postcss-svgo: 6.0.3(postcss@8.5.19) - postcss-unique-selectors: 6.0.4(postcss@8.5.19) + css-declaration-sorter: 7.4.0(postcss@8.5.25) + cssnano-utils: 4.0.2(postcss@8.5.25) + postcss: 8.5.25 + postcss-calc: 9.0.1(postcss@8.5.25) + postcss-colormin: 6.1.0(postcss@8.5.25) + postcss-convert-values: 6.1.0(postcss@8.5.25) + postcss-discard-comments: 6.0.2(postcss@8.5.25) + postcss-discard-duplicates: 6.0.3(postcss@8.5.25) + postcss-discard-empty: 6.0.3(postcss@8.5.25) + postcss-discard-overridden: 6.0.2(postcss@8.5.25) + postcss-merge-longhand: 6.0.5(postcss@8.5.25) + postcss-merge-rules: 6.1.1(postcss@8.5.25) + postcss-minify-font-values: 6.1.0(postcss@8.5.25) + postcss-minify-gradients: 6.0.3(postcss@8.5.25) + postcss-minify-params: 6.1.0(postcss@8.5.25) + postcss-minify-selectors: 6.0.4(postcss@8.5.25) + postcss-normalize-charset: 6.0.2(postcss@8.5.25) + postcss-normalize-display-values: 6.0.2(postcss@8.5.25) + postcss-normalize-positions: 6.0.2(postcss@8.5.25) + postcss-normalize-repeat-style: 6.0.2(postcss@8.5.25) + postcss-normalize-string: 6.0.2(postcss@8.5.25) + postcss-normalize-timing-functions: 6.0.2(postcss@8.5.25) + postcss-normalize-unicode: 6.1.0(postcss@8.5.25) + postcss-normalize-url: 6.0.2(postcss@8.5.25) + postcss-normalize-whitespace: 6.0.2(postcss@8.5.25) + postcss-ordered-values: 6.0.2(postcss@8.5.25) + postcss-reduce-initial: 6.1.0(postcss@8.5.25) + postcss-reduce-transforms: 6.0.2(postcss@8.5.25) + postcss-svgo: 6.0.3(postcss@8.5.25) + postcss-unique-selectors: 6.0.4(postcss@8.5.25) - cssnano-utils@4.0.2(postcss@8.5.19): + cssnano-utils@4.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - cssnano@6.1.2(postcss@8.5.19): + cssnano@6.1.2(postcss@8.5.25): dependencies: - cssnano-preset-default: 6.1.2(postcss@8.5.19) + cssnano-preset-default: 6.1.2(postcss@8.5.25) lilconfig: 3.1.3 - postcss: 8.5.19 + postcss: 8.5.25 csso@5.0.5: dependencies: @@ -20230,17 +20090,28 @@ snapshots: debounce@2.2.0: {} - debug@2.6.9: + debug@2.6.9(supports-color@8.1.1): dependencies: ms: 2.0.0 + optionalDependencies: + supports-color: 8.1.1 - debug@4.3.7: + debug@4.3.7(supports-color@8.1.1): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 debug@4.4.3: dependencies: ms: 2.1.3 + optional: true + + debug@4.4.3(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 decamelize@1.2.0: {} @@ -20323,7 +20194,7 @@ snapshots: dependencies: address: 2.0.3 - devalue@5.8.1: {} + devalue@5.8.2: {} devlop@1.1.0: dependencies: @@ -20360,29 +20231,29 @@ snapshots: dependencies: yaml: 2.9.0 - docker-modem@5.0.7: + docker-modem@5.0.7(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) readable-stream: 3.6.2 split-ca: 1.0.1 ssh2: 1.17.0 transitivePeerDependencies: - supports-color - dockerode@5.0.1: + dockerode@5.0.1(supports-color@8.1.1): dependencies: '@balena/dockerignore': 1.0.2 '@grpc/grpc-js': 1.14.4 '@grpc/proto-loader': 0.7.15 - docker-modem: 5.0.7 + docker-modem: 5.0.7(supports-color@8.1.1) protobufjs: 7.6.5 tar-fs: 2.1.5 transitivePeerDependencies: - supports-color - docusaurus-lunr-search@3.6.0(@docusaurus/core@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + docusaurus-lunr-search@3.6.0(@docusaurus/core@3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@typescript/typescript6@6.0.2)(postcss@8.5.19)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@docusaurus/core': 3.10.2(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8))(@swc/core@1.15.46(@swc/helpers@0.5.23))(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(uglify-js@3.19.3) autocomplete.js: 0.37.1 clsx: 2.1.1 gauge: 3.0.2 @@ -20393,8 +20264,8 @@ snapshots: lunr-languages: 1.20.0 mark.js: 8.11.1 minimatch: 3.1.5 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) rehype-parse: 7.0.1 to-vfile: 6.1.0 unified: 9.2.2 @@ -20501,10 +20372,10 @@ snapshots: dependencies: once: 1.4.0 - engine.io-client@6.6.5: + engine.io-client@6.6.5(supports-color@8.1.1): dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) engine.io-parser: 5.2.3 ws: 8.20.1 xmlhttprequest-ssl: 2.1.2 @@ -20515,7 +20386,7 @@ snapshots: engine.io-parser@5.2.3: {} - engine.io@6.6.8: + engine.io@6.6.8(supports-color@8.1.1): dependencies: '@types/cors': 2.8.19 '@types/node': 24.13.3 @@ -20524,7 +20395,7 @@ snapshots: base64id: 2.0.0 cookie: 0.7.2 cors: 2.8.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) engine.io-parser: 5.2.3 ws: 8.20.1 transitivePeerDependencies: @@ -20532,12 +20403,7 @@ snapshots: - supports-color - utf-8-validate - enhanced-resolve@5.21.6: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.3 - - enhanced-resolve@5.24.2: + enhanced-resolve@5.24.4: dependencies: graceful-fs: 4.2.11 tapable: 2.3.3 @@ -20614,7 +20480,7 @@ snapshots: esast-util-from-js@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 - acorn: 8.17.0 + acorn: 8.18.0 esast-util-from-estree: 2.0.0 vfile-message: 4.0.3 @@ -20714,77 +20580,77 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.7.0(jiti@2.7.0)): + eslint-config-prettier@10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)): dependencies: - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) - eslint-plugin-better-tailwindcss@4.6.1(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0))(tailwindcss@4.3.2): + eslint-plugin-better-tailwindcss@4.7.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(tailwindcss@4.3.3): dependencies: - '@eslint/css-tree': 4.0.4 + '@eslint/css-tree': 4.0.5 '@valibot/to-json-schema': 1.7.1(valibot@1.4.2(@typescript/typescript6@6.0.2)) - enhanced-resolve: 5.24.2 + enhanced-resolve: 5.24.4 jiti: 2.7.0 synckit: 0.11.13 tailwind-csstree: 0.3.3 - tailwindcss: 4.3.2 + tailwindcss: 4.3.3 tsconfig-paths-webpack-plugin: 4.2.0 valibot: 1.4.2(@typescript/typescript6@6.0.2) optionalDependencies: - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) transitivePeerDependencies: - '@eslint/css' - typescript - eslint-plugin-compat@7.0.2(eslint@10.7.0(jiti@2.7.0)): + eslint-plugin-compat@7.0.2(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)): dependencies: '@mdn/browser-compat-data': 6.1.5 ast-metadata-inferer: 0.8.1 browserslist: 4.28.6 - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) find-up: 5.0.0 globals: 15.15.0 lodash.memoize: 4.1.2 semver: 7.8.5 - eslint-plugin-prettier@5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.7.0(jiti@2.7.0)))(eslint@10.7.0(jiti@2.7.0))(prettier@3.9.5): + eslint-plugin-prettier@5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)))(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(prettier@3.9.6): dependencies: - eslint: 10.7.0(jiti@2.7.0) - prettier: 3.9.5 + eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) + prettier: 3.9.6 prettier-linter-helpers: 1.0.1 synckit: 0.11.13 optionalDependencies: '@types/eslint': 9.6.1 - eslint-config-prettier: 10.1.8(eslint@10.7.0(jiti@2.7.0)) + eslint-config-prettier: 10.1.8(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) - eslint-plugin-svelte@3.20.0(eslint@10.7.0(jiti@2.7.0))(svelte@5.56.5(@typescript-eslint/types@8.64.0)): + eslint-plugin-svelte@3.22.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(svelte@5.56.8(@typescript-eslint/types@8.65.0)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) '@jridgewell/sourcemap-codec': 1.5.5 - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) esutils: 2.0.3 globals: 16.5.0 known-css-properties: 0.37.0 - postcss: 8.5.19 - postcss-load-config: 3.1.4(postcss@8.5.19) - postcss-safe-parser: 7.0.1(postcss@8.5.19) + postcss: 8.5.25 + postcss-load-config: 3.1.4(postcss@8.5.25) + postcss-safe-parser: 7.0.1(postcss@8.5.25) semver: 7.8.5 - svelte-eslint-parser: 1.8.0(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + svelte-eslint-parser: 1.8.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)) optionalDependencies: - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) transitivePeerDependencies: - ts-node - eslint-plugin-unicorn@72.0.0(eslint@10.7.0(jiti@2.7.0)): + eslint-plugin-unicorn@72.0.0(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)) - '@eslint/css-tree': 4.0.4 + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) + '@eslint/css-tree': 4.0.5 browserslist: 4.28.6 change-case: 5.4.4 ci-info: 4.4.0 core-js-compat: 3.49.0 detect-indent: 7.0.2 entities: 4.5.0 - eslint: 10.7.0(jiti@2.7.0) + eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) find-up-simple: 1.0.1 globals: 17.7.0 indent-string: 5.0.0 @@ -20821,12 +20687,12 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.7.0(jiti@2.7.0): + eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.5 - '@eslint/config-helpers': 0.6.0 + '@eslint/config-array': 0.23.5(supports-color@8.1.1) + '@eslint/config-helpers': 0.7.0 '@eslint/core': 1.2.1 '@eslint/plugin-kit': 0.7.2 '@humanfs/node': 0.16.8 @@ -20835,7 +20701,7 @@ snapshots: '@types/estree': 1.0.9 ajv: 6.15.0 cross-spawn: 7.0.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 @@ -20850,7 +20716,7 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 - minimatch: 10.2.5 + minimatch: 10.2.6 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: @@ -20869,14 +20735,14 @@ snapshots: espree@10.4.0: dependencies: - acorn: 8.17.0 - acorn-jsx: 5.3.2(acorn@8.17.0) + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) eslint-visitor-keys: 4.2.1 espree@11.2.0: dependencies: - acorn: 8.17.0 - acorn-jsx: 5.3.2(acorn@8.17.0) + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) eslint-visitor-keys: 5.0.1 esprima@4.0.1: {} @@ -20885,11 +20751,11 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@2.2.13(@typescript-eslint/types@8.64.0): + esrap@2.2.13(@typescript-eslint/types@8.65.0): dependencies: '@jridgewell/sourcemap-codec': 1.5.5 optionalDependencies: - '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/types': 8.65.0 esrecurse@4.3.0: dependencies: @@ -21003,21 +20869,21 @@ snapshots: exponential-backoff@3.1.3: {} - express@4.22.2: + express@4.22.2(supports-color@8.1.1): dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.6 + body-parser: 1.20.6(supports-color@8.1.1) content-disposition: 0.5.4 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.0.7 - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 1.3.2 + finalhandler: 1.3.2(supports-color@8.1.1) fresh: 0.5.2 http-errors: 2.0.1 merge-descriptors: 1.0.3 @@ -21029,8 +20895,8 @@ snapshots: qs: 6.15.3 range-parser: 1.2.1 safe-buffer: 5.2.1 - send: 0.19.2 - serve-static: 1.16.3 + send: 0.19.2(supports-color@8.1.1) + serve-static: 1.16.3(supports-color@8.1.1) setprototypeof: 1.2.0 statuses: 2.0.2 type-is: 1.6.18 @@ -21039,20 +20905,20 @@ snapshots: transitivePeerDependencies: - supports-color - express@5.2.1: + express@5.2.1(supports-color@8.1.1): dependencies: accepts: 2.0.0 - body-parser: 2.3.0 + body-parser: 2.3.0(supports-color@8.1.1) content-disposition: 1.1.0 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 2.1.1 + finalhandler: 2.1.1(supports-color@8.1.1) fresh: 2.0.0 http-errors: 2.0.1 merge-descriptors: 2.0.0 @@ -21063,9 +20929,9 @@ snapshots: proxy-addr: 2.0.7 qs: 6.15.3 range-parser: 1.3.0 - router: 2.2.0 - send: 1.2.1 - serve-static: 2.2.1 + router: 2.2.0(supports-color@8.1.1) + send: 1.2.1(supports-color@8.1.1) + serve-static: 2.2.1(supports-color@8.1.1) statuses: 2.0.2 type-is: 2.1.0 vary: 1.1.2 @@ -21082,10 +20948,10 @@ snapshots: extend@3.0.2: {} - fabric@7.4.0: + fabric@7.4.0(supports-color@8.1.1): optionalDependencies: canvas: 3.2.3 - jsdom: 26.1.0(canvas@3.2.3) + jsdom: 26.1.0(canvas@3.2.3)(supports-color@8.1.1) transitivePeerDependencies: - bufferutil - supports-color @@ -21116,7 +20982,7 @@ snapshots: fast-safe-stringify@2.1.1: {} - fast-uri@3.1.3: {} + fast-uri@3.1.4: {} fastq@1.20.1: dependencies: @@ -21148,19 +21014,19 @@ snapshots: dependencies: flat-cache: 4.0.1 - file-loader@6.2.0(webpack@5.108.4(postcss@8.5.19)): + file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.108.4(postcss@8.5.19) + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) file-source@0.6.1: dependencies: stream-source: 0.3.5 - file-type@21.3.4: + file-type@21.3.4(supports-color@8.1.1): dependencies: - '@tokenizer/inflate': 0.4.1 + '@tokenizer/inflate': 0.4.1(supports-color@8.1.1) strtok3: 10.3.5 token-types: 6.1.2 uint8array-extras: 1.5.0 @@ -21171,9 +21037,9 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@1.3.2: + finalhandler@1.3.2(supports-color@8.1.1): dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -21183,9 +21049,9 @@ snapshots: transitivePeerDependencies: - supports-color - finalhandler@2.1.1: + finalhandler@2.1.1(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -21218,26 +21084,28 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.4.2 + flatted: 3.4.3 keyv: 4.5.4 flat@5.0.2: {} - flatted@3.4.2: {} + flatted@3.4.3: {} fluent-ffmpeg@2.1.3: dependencies: async: 0.2.10 which: 1.3.1 - follow-redirects@1.16.0: {} + follow-redirects@1.16.0(debug@4.4.3(supports-color@8.1.1)): + optionalDependencies: + debug: 4.4.3(supports-color@8.1.1) foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.2(@swc/core@1.15.43(@swc/helpers@0.5.23))(esbuild@0.28.1)(lightningcss@1.33.0)): + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): dependencies: '@babel/code-frame': 7.29.7 chalk: 4.1.2 @@ -21252,7 +21120,7 @@ snapshots: semver: 7.8.5 tapable: 2.3.3 typescript: 5.9.3 - webpack: 5.106.2(@swc/core@1.15.43(@swc/helpers@0.5.23))(esbuild@0.28.1)(lightningcss@1.33.0) + webpack: 5.106.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) form-data-encoder@2.1.4: {} @@ -21411,7 +21279,7 @@ snapshots: glob@13.0.6: dependencies: - minimatch: 10.2.5 + minimatch: 10.2.6 minipass: 7.1.3 path-scurry: 2.0.2 @@ -21496,7 +21364,7 @@ snapshots: optionalDependencies: uglify-js: 3.19.3 - happy-dom@20.10.6: + happy-dom@20.11.1: dependencies: '@types/node': 24.13.3 '@types/whatwg-mimetype': 3.0.2 @@ -21592,7 +21460,7 @@ snapshots: unist-util-visit: 2.0.3 zwitch: 1.0.5 - hast-util-to-estree@3.1.3: + hast-util-to-estree@3.1.3(supports-color@8.1.1): dependencies: '@types/estree': 1.0.9 '@types/estree-jsx': 1.0.5 @@ -21602,9 +21470,9 @@ snapshots: estree-util-attach-comments: 3.0.0 estree-util-is-identifier-name: 3.0.0 hast-util-whitespace: 3.0.0 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 + mdast-util-mdx-expression: 2.0.1(supports-color@8.1.1) + mdast-util-mdx-jsx: 3.2.0(supports-color@8.1.1) + mdast-util-mdxjs-esm: 2.0.1(supports-color@8.1.1) property-information: 7.2.0 space-separated-tokens: 2.0.2 style-to-js: 1.1.21 @@ -21613,7 +21481,7 @@ snapshots: transitivePeerDependencies: - supports-color - hast-util-to-jsx-runtime@2.3.6: + hast-util-to-jsx-runtime@2.3.6(supports-color@8.1.1): dependencies: '@types/estree': 1.0.9 '@types/hast': 3.0.5 @@ -21622,9 +21490,9 @@ snapshots: devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 hast-util-whitespace: 3.0.0 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 + mdast-util-mdx-expression: 2.0.1(supports-color@8.1.1) + mdast-util-mdx-jsx: 3.2.0(supports-color@8.1.1) + mdast-util-mdxjs-esm: 2.0.1(supports-color@8.1.1) property-information: 7.2.0 space-separated-tokens: 2.0.2 style-to-js: 1.1.21 @@ -21751,7 +21619,7 @@ snapshots: html-void-elements@3.0.0: {} - html-webpack-plugin@5.6.7(webpack@5.108.4(postcss@8.5.19)): + html-webpack-plugin@5.6.7(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -21759,7 +21627,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.3.3 optionalDependencies: - webpack: 5.108.4(postcss@8.5.19) + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) html5parser@3.0.0: {} @@ -21812,10 +21680,18 @@ snapshots: - supports-color optional: true - http-proxy-middleware@2.0.10(@types/express@4.17.25): + http-proxy-agent@7.0.2(supports-color@8.1.1): + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + optional: true + + http-proxy-middleware@2.0.10(@types/express@4.17.25)(debug@4.4.3(supports-color@8.1.1)): dependencies: '@types/http-proxy': 1.17.17 - http-proxy: 1.18.1 + http-proxy: 1.18.1(debug@4.4.3(supports-color@8.1.1)) is-glob: 4.0.3 is-plain-obj: 3.0.0 micromatch: 4.0.8 @@ -21824,10 +21700,10 @@ snapshots: transitivePeerDependencies: - debug - http-proxy@1.18.1: + http-proxy@1.18.1(debug@4.4.3(supports-color@8.1.1)): dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.16.0 + follow-redirects: 1.16.0(debug@4.4.3(supports-color@8.1.1)) requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -21837,10 +21713,10 @@ snapshots: quick-lru: 5.1.1 resolve-alpn: 1.2.1 - https-proxy-agent@5.0.1: + https-proxy-agent@5.0.1(supports-color@8.1.1): dependencies: - agent-base: 6.0.2 - debug: 4.4.3 + agent-base: 6.0.2(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -21852,6 +21728,14 @@ snapshots: - supports-color optional: true + https-proxy-agent@7.0.6(supports-color@8.1.1): + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + optional: true + human-signals@2.1.0: {} hyperdyperid@1.2.0: {} @@ -21872,9 +21756,9 @@ snapshots: dependencies: safer-buffer: 2.1.2 - icss-utils@5.1.0(postcss@8.5.19): + icss-utils@5.1.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 identifier-regex@1.1.0: dependencies: @@ -21892,7 +21776,7 @@ snapshots: immediate@3.3.0: {} - immutable-json-patch@6.0.2: {} + immutable-json-patch@6.0.3: {} immutable@5.1.6: {} @@ -21903,8 +21787,8 @@ snapshots: import-in-the-middle@3.0.2: dependencies: - acorn: 8.17.0 - acorn-import-attributes: 1.9.5(acorn@8.17.0) + acorn: 8.18.0 + acorn-import-attributes: 1.9.5(acorn@8.18.0) cjs-module-lexer: 2.2.0 module-details-from-path: 1.0.4 @@ -21973,11 +21857,11 @@ snapshots: dependencies: loose-envify: 1.4.0 - ioredis@5.11.1: + ioredis@5.11.1(supports-color@8.1.1): dependencies: '@ioredis/commands': 1.10.0 cluster-key-slot: 1.1.1 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) denque: 2.1.0 redis-errors: 1.2.0 redis-parser: 3.0.0 @@ -22182,7 +22066,7 @@ snapshots: '@sideway/formula': 3.0.1 '@sideway/pinpoint': 2.0.0 - jose@6.2.3: {} + jose@6.2.4: {} js-tokens@10.0.0: {} @@ -22194,6 +22078,10 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@5.2.1: + dependencies: + argparse: 2.0.1 + jsdom@26.1.0(canvas@3.2.3): dependencies: cssstyle: 4.6.0 @@ -22224,6 +22112,36 @@ snapshots: - utf-8-validate optional: true + jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1): + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2(supports-color@8.1.1) + https-proxy-agent: 7.0.6(supports-color@8.1.1) + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.24 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.21.1 + xml-name-validator: 5.0.0 + optionalDependencies: + canvas: 3.2.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + optional: true + jsep@1.4.0: {} jsesc@3.1.0: {} @@ -22260,7 +22178,7 @@ snapshots: '@jsep-plugin/regex': 1.0.4(jsep@1.4.0) jsep: 1.4.0 - jsonrepair@3.14.0: {} + jsonrepair@3.15.0: {} jsonwebtoken@9.0.3: dependencies: @@ -22640,13 +22558,13 @@ snapshots: math-intrinsics@1.1.0: {} - mdast-util-directive@3.1.0: + mdast-util-directive@3.1.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 ccount: 2.0.1 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 parse-entities: 4.0.2 stringify-entities: 4.0.4 @@ -22661,14 +22579,14 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 - mdast-util-from-markdown@2.0.3: + mdast-util-from-markdown@2.0.3(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 decode-named-character-reference: 1.3.0 devlop: 1.1.0 mdast-util-to-string: 4.0.0 - micromark: 4.0.2 + micromark: 4.0.2(supports-color@8.1.1) micromark-util-decode-numeric-character-reference: 2.0.2 micromark-util-decode-string: 2.0.1 micromark-util-normalize-identifier: 2.0.1 @@ -22678,12 +22596,12 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-frontmatter@2.0.1: + mdast-util-frontmatter@2.0.1(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 escape-string-regexp: 5.0.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 micromark-extension-frontmatter: 2.0.0 transitivePeerDependencies: @@ -22697,67 +22615,67 @@ snapshots: mdast-util-find-and-replace: 3.0.2 micromark-util-character: 2.1.1 - mdast-util-gfm-footnote@2.1.0: + mdast-util-gfm-footnote@2.1.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 micromark-util-normalize-identifier: 2.0.1 transitivePeerDependencies: - supports-color - mdast-util-gfm-strikethrough@2.0.0: + mdast-util-gfm-strikethrough@2.0.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-gfm-table@2.0.0: + mdast-util-gfm-table@2.0.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 markdown-table: 3.0.4 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-gfm-task-list-item@2.0.0: + mdast-util-gfm-task-list-item@2.0.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-gfm@3.1.0: + mdast-util-gfm@3.1.0(supports-color@8.1.1): dependencies: - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-gfm-autolink-literal: 2.0.1 - mdast-util-gfm-footnote: 2.1.0 - mdast-util-gfm-strikethrough: 2.0.0 - mdast-util-gfm-table: 2.0.0 - mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-gfm-footnote: 2.1.0(supports-color@8.1.1) + mdast-util-gfm-strikethrough: 2.0.0(supports-color@8.1.1) + mdast-util-gfm-table: 2.0.0(supports-color@8.1.1) + mdast-util-gfm-task-list-item: 2.0.0(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-mdx-expression@2.0.1: + mdast-util-mdx-expression@2.0.1(supports-color@8.1.1): dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.5 '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-mdx-jsx@3.2.0: + mdast-util-mdx-jsx@3.2.0(supports-color@8.1.1): dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.5 @@ -22765,7 +22683,7 @@ snapshots: '@types/unist': 3.0.3 ccount: 2.0.1 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 parse-entities: 4.0.2 stringify-entities: 4.0.4 @@ -22774,23 +22692,23 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-mdx@3.0.0: + mdast-util-mdx@3.0.0(supports-color@8.1.1): dependencies: - mdast-util-from-markdown: 2.0.3 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) + mdast-util-mdx-expression: 2.0.1(supports-color@8.1.1) + mdast-util-mdx-jsx: 3.2.0(supports-color@8.1.1) + mdast-util-mdxjs-esm: 2.0.1(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-mdxjs-esm@2.0.1: + mdast-util-mdxjs-esm@2.0.1(supports-color@8.1.1): dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.5 '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color @@ -22832,11 +22750,11 @@ snapshots: mdn-data@2.0.30: {} - mdn-data@2.28.1: {} + mdn-data@2.29.0: {} - media-chrome@4.19.2(react@19.2.7): + media-chrome@4.19.2(react@19.2.8): dependencies: - ce-la-react: 0.3.2(react@19.2.7) + ce-la-react: 0.3.2(react@19.2.8) transitivePeerDependencies: - react @@ -23050,8 +22968,8 @@ snapshots: micromark-extension-mdxjs@3.0.0: dependencies: - acorn: 8.17.0 - acorn-jsx: 5.3.2(acorn@8.17.0) + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) micromark-extension-mdx-expression: 3.0.1 micromark-extension-mdx-jsx: 3.0.2 micromark-extension-mdx-md: 2.0.0 @@ -23187,10 +23105,10 @@ snapshots: micromark-util-types@2.0.2: {} - micromark@4.0.2: + micromark@4.0.2(supports-color@8.1.1): dependencies: '@types/debug': 4.1.13 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) decode-named-character-reference: 1.3.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -23246,17 +23164,17 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.10.2(webpack@5.108.4(postcss@8.5.19)): + mini-css-extract-plugin@2.10.2(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): dependencies: schema-utils: 4.3.3 tapable: 2.3.3 - webpack: 5.108.4(postcss@8.5.19) + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) minimalistic-assert@1.0.1: {} - minimatch@10.2.5: + minimatch@10.2.6: dependencies: - brace-expansion: 5.0.7 + brace-expansion: 5.0.8 minimatch@3.1.5: dependencies: @@ -23272,28 +23190,23 @@ snapshots: minimist@1.2.8: {} - minimizer-webpack-plugin@5.6.1(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(webpack@5.108.4(postcss@8.5.19)): + minimizer-webpack-plugin@5.6.1(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.49.0 - webpack: 5.108.4(postcss@8.5.19) + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) optionalDependencies: + '@swc/core': 1.15.46(@swc/helpers@0.5.23) clean-css: 5.3.3 - cssnano: 6.1.2(postcss@8.5.19) + cssnano: 6.1.2(postcss@8.5.25) + csso: 5.0.5 + esbuild: 0.28.1 html-minifier-terser: 7.2.0 - postcss: 8.5.19 - - minimizer-webpack-plugin@5.6.1(postcss@8.5.19)(webpack@5.108.4(postcss@8.5.19)): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - jest-worker: 27.5.1 - schema-utils: 4.3.3 - terser: 5.49.0 - webpack: 5.108.4(postcss@8.5.19) - optionalDependencies: - postcss: 8.5.19 + lightningcss: 1.33.0 + postcss: 8.5.25 + uglify-js: 3.19.3 minipass@3.3.6: dependencies: @@ -23385,7 +23298,7 @@ snapshots: nanoid@3.3.16: {} - nanoid@5.1.16: {} + nanoid@6.0.0: {} napi-build-utils@2.0.0: optional: true @@ -23409,12 +23322,12 @@ snapshots: neo-async@2.6.2: {} - nest-commander@3.20.1(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@types/inquirer@8.2.13)(@types/node@24.13.3)(@typescript/typescript6@6.0.2): + nest-commander@3.20.1(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(@types/inquirer@8.2.13)(@types/node@24.13.3)(@typescript/typescript6@6.0.2): dependencies: '@fig/complete-commander': 3.2.0(commander@11.1.0) - '@golevelup/nestjs-discovery': 5.0.0(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28) - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@golevelup/nestjs-discovery': 5.0.0(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28) + '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) + '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@types/inquirer': 8.2.13 commander: 11.1.0 cosmiconfig: 8.3.6(@typescript/typescript6@6.0.2) @@ -23423,38 +23336,38 @@ snapshots: - '@types/node' - typescript - nestjs-cls@6.2.1(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2): + nestjs-cls@6.2.1(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2): dependencies: - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) + '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 rxjs: 7.8.2 - nestjs-kysely@3.1.2(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(kysely@0.28.17)(reflect-metadata@0.2.2): + nestjs-kysely@3.1.2(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(kysely@0.28.17)(reflect-metadata@0.2.2): dependencies: - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) + '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) kysely: 0.28.17 reflect-metadata: 0.2.2 tslib: 2.8.1 - nestjs-otel@8.1.0(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(rxjs@7.8.2): + nestjs-otel@8.1.0(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(rxjs@7.8.2): dependencies: - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) + '@nestjs/core': 11.1.28(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/platform-express@11.1.28)(@nestjs/websockets@11.1.28)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@opentelemetry/api': 1.9.1 '@opentelemetry/host-metrics': 0.38.3(@opentelemetry/api@1.9.1) rxjs: 7.8.2 tslib: 2.8.1 - nestjs-zod@5.4.0(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.4.5(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@typescript/typescript6@6.0.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6): + nestjs-zod@5.5.0(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/swagger@11.4.6(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(@typescript/typescript6@6.0.2)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6): dependencies: - '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1) deepmerge: 4.3.1 rxjs: 7.8.2 zod: 4.3.6 optionalDependencies: - '@nestjs/swagger': 11.4.5(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)(@typescript/typescript6@6.0.2)(reflect-metadata@0.2.2) + '@nestjs/swagger': 11.4.6(@nestjs/common@11.1.28(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@8.1.1))(@nestjs/core@11.1.28)(@typescript/typescript6@6.0.2)(reflect-metadata@0.2.2) next-tick@1.1.0: {} @@ -23553,11 +23466,11 @@ snapshots: dependencies: boolbase: 1.0.0 - null-loader@4.0.1(webpack@5.108.4(postcss@8.5.19)): + null-loader@4.0.1(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.108.4(postcss@8.5.19) + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) nwsapi@2.2.24: optional: true @@ -23593,18 +23506,18 @@ snapshots: obug@2.1.3: {} - oidc-provider@9.9.1: + oidc-provider@9.10.0(supports-color@8.1.1): dependencies: '@koa/cors': 5.0.0 - '@koa/router': 15.7.0(koa@3.2.1) - debug: 4.4.3 + '@koa/router': 15.7.0(koa@3.2.1)(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) eta: 4.6.0 - jose: 6.2.3 + jose: 6.2.4 jsesc: 3.1.0 koa: 3.2.1 - nanoid: 5.1.16 + nanoid: 6.0.0 quick-lru: 7.3.0 - raw-body: 3.0.2 + raw-body: 4.0.0 transitivePeerDependencies: - supports-color @@ -23652,7 +23565,7 @@ snapshots: openid-client@6.8.4: dependencies: - jose: 6.2.3 + jose: 6.2.4 oauth4webapi: 3.8.6 optionator@0.9.4: @@ -23931,11 +23844,11 @@ snapshots: pvutils: 1.1.5 tslib: 2.8.1 - playwright-core@1.61.1: {} + playwright-core@1.62.0: {} - playwright@1.61.1: + playwright@1.62.0: dependencies: - playwright-core: 1.61.1 + playwright-core: 1.62.0 optionalDependencies: fsevents: 2.3.2 @@ -23969,448 +23882,448 @@ snapshots: path-data-parser: 0.1.0 points-on-curve: 0.2.0 - postcss-attribute-case-insensitive@7.0.1(postcss@8.5.19): + postcss-attribute-case-insensitive@7.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 - postcss-calc@9.0.1(postcss@8.5.19): + postcss-calc@9.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 6.1.4 postcss-value-parser: 4.2.0 - postcss-clamp@4.1.0(postcss@8.5.19): + postcss-clamp@4.1.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-color-functional-notation@7.0.12(postcss@8.5.19): + postcss-color-functional-notation@7.0.12(postcss@8.5.25): dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 - postcss-color-hex-alpha@10.0.0(postcss@8.5.19): + postcss-color-hex-alpha@10.0.0(postcss@8.5.25): dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-color-rebeccapurple@10.0.0(postcss@8.5.19): + postcss-color-rebeccapurple@10.0.0(postcss@8.5.25): dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-colormin@6.1.0(postcss@8.5.19): + postcss-colormin@6.1.0(postcss@8.5.25): dependencies: browserslist: 4.28.6 caniuse-api: 3.0.0 colord: 2.9.3 - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-convert-values@6.1.0(postcss@8.5.19): + postcss-convert-values@6.1.0(postcss@8.5.25): dependencies: browserslist: 4.28.6 - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-custom-media@11.0.6(postcss@8.5.19): + postcss-custom-media@11.0.6(postcss@8.5.25): dependencies: '@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 '@csstools/media-query-list-parser': 4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - postcss: 8.5.19 + postcss: 8.5.25 - postcss-custom-properties@14.0.6(postcss@8.5.19): + postcss-custom-properties@14.0.6(postcss@8.5.25): dependencies: '@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-custom-selectors@8.0.5(postcss@8.5.19): + postcss-custom-selectors@8.0.5(postcss@8.5.25): dependencies: '@csstools/cascade-layer-name-parser': 2.0.5(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 - postcss-dir-pseudo-class@9.0.1(postcss@8.5.19): + postcss-dir-pseudo-class@9.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 - postcss-discard-comments@6.0.2(postcss@8.5.19): + postcss-discard-comments@6.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-discard-duplicates@6.0.3(postcss@8.5.19): + postcss-discard-duplicates@6.0.3(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-discard-empty@6.0.3(postcss@8.5.19): + postcss-discard-empty@6.0.3(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-discard-overridden@6.0.2(postcss@8.5.19): + postcss-discard-overridden@6.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-discard-unused@6.0.5(postcss@8.5.19): + postcss-discard-unused@6.0.5(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 6.1.4 - postcss-double-position-gradients@6.0.4(postcss@8.5.19): + postcss-double-position-gradients@6.0.4(postcss@8.5.25): dependencies: - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-focus-visible@10.0.1(postcss@8.5.19): + postcss-focus-visible@10.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 - postcss-focus-within@9.0.1(postcss@8.5.19): + postcss-focus-within@9.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 - postcss-font-variant@5.0.0(postcss@8.5.19): + postcss-font-variant@5.0.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-gap-properties@6.0.0(postcss@8.5.19): + postcss-gap-properties@6.0.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-image-set-function@7.0.0(postcss@8.5.19): + postcss-image-set-function@7.0.0(postcss@8.5.25): dependencies: - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-import@15.1.0(postcss@8.5.19): + postcss-import@15.1.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.12 - postcss-js@4.1.0(postcss@8.5.19): + postcss-js@4.1.0(postcss@8.5.25): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.19 + postcss: 8.5.25 - postcss-lab-function@7.0.12(postcss@8.5.19): + postcss-lab-function@7.0.12(postcss@8.5.25): dependencies: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.19) - '@csstools/utilities': 2.0.0(postcss@8.5.19) - postcss: 8.5.19 + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) + '@csstools/utilities': 2.0.0(postcss@8.5.25) + postcss: 8.5.25 - postcss-load-config@3.1.4(postcss@8.5.19): + postcss-load-config@3.1.4(postcss@8.5.25): dependencies: lilconfig: 2.1.0 yaml: 1.10.3 optionalDependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.19)(tsx@4.23.1)(yaml@2.9.0): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.25)(tsx@4.23.1)(yaml@2.9.0): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 1.21.7 - postcss: 8.5.19 + postcss: 8.5.25 tsx: 4.23.1 yaml: 2.9.0 - postcss-loader@7.3.4(@typescript/typescript6@6.0.2)(postcss@8.5.19)(webpack@5.108.4(postcss@8.5.19)): + postcss-loader@7.3.4(@typescript/typescript6@6.0.2)(postcss@8.5.25)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): dependencies: cosmiconfig: 8.3.6(@typescript/typescript6@6.0.2) jiti: 1.21.7 - postcss: 8.5.19 + postcss: 8.5.25 semver: 7.8.5 - webpack: 5.108.4(postcss@8.5.19) + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) transitivePeerDependencies: - typescript - postcss-logical@8.1.0(postcss@8.5.19): + postcss-logical@8.1.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-merge-idents@6.0.3(postcss@8.5.19): + postcss-merge-idents@6.0.3(postcss@8.5.25): dependencies: - cssnano-utils: 4.0.2(postcss@8.5.19) - postcss: 8.5.19 + cssnano-utils: 4.0.2(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-merge-longhand@6.0.5(postcss@8.5.19): + postcss-merge-longhand@6.0.5(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - stylehacks: 6.1.1(postcss@8.5.19) + stylehacks: 6.1.1(postcss@8.5.25) - postcss-merge-rules@6.1.1(postcss@8.5.19): + postcss-merge-rules@6.1.1(postcss@8.5.25): dependencies: browserslist: 4.28.6 caniuse-api: 3.0.0 - cssnano-utils: 4.0.2(postcss@8.5.19) - postcss: 8.5.19 + cssnano-utils: 4.0.2(postcss@8.5.25) + postcss: 8.5.25 postcss-selector-parser: 6.1.4 - postcss-minify-font-values@6.1.0(postcss@8.5.19): + postcss-minify-font-values@6.1.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-minify-gradients@6.0.3(postcss@8.5.19): + postcss-minify-gradients@6.0.3(postcss@8.5.25): dependencies: colord: 2.9.3 - cssnano-utils: 4.0.2(postcss@8.5.19) - postcss: 8.5.19 + cssnano-utils: 4.0.2(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-minify-params@6.1.0(postcss@8.5.19): + postcss-minify-params@6.1.0(postcss@8.5.25): dependencies: browserslist: 4.28.6 - cssnano-utils: 4.0.2(postcss@8.5.19) - postcss: 8.5.19 + cssnano-utils: 4.0.2(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-minify-selectors@6.0.4(postcss@8.5.19): + postcss-minify-selectors@6.0.4(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 6.1.4 - postcss-modules-extract-imports@3.1.0(postcss@8.5.19): + postcss-modules-extract-imports@3.1.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-modules-local-by-default@4.2.0(postcss@8.5.19): + postcss-modules-local-by-default@4.2.0(postcss@8.5.25): dependencies: - icss-utils: 5.1.0(postcss@8.5.19) - postcss: 8.5.19 + icss-utils: 5.1.0(postcss@8.5.25) + postcss: 8.5.25 postcss-selector-parser: 7.1.4 postcss-value-parser: 4.2.0 - postcss-modules-scope@3.2.1(postcss@8.5.19): + postcss-modules-scope@3.2.1(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 - postcss-modules-values@4.0.0(postcss@8.5.19): + postcss-modules-values@4.0.0(postcss@8.5.25): dependencies: - icss-utils: 5.1.0(postcss@8.5.19) - postcss: 8.5.19 + icss-utils: 5.1.0(postcss@8.5.25) + postcss: 8.5.25 - postcss-nested@6.2.0(postcss@8.5.19): + postcss-nested@6.2.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 6.1.4 - postcss-nesting@13.0.2(postcss@8.5.19): + postcss-nesting@13.0.2(postcss@8.5.25): dependencies: '@csstools/selector-resolve-nested': 3.1.0(postcss-selector-parser@7.1.4) '@csstools/selector-specificity': 5.0.0(postcss-selector-parser@7.1.4) - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 - postcss-normalize-charset@6.0.2(postcss@8.5.19): + postcss-normalize-charset@6.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-normalize-display-values@6.0.2(postcss@8.5.19): + postcss-normalize-display-values@6.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-normalize-positions@6.0.2(postcss@8.5.19): + postcss-normalize-positions@6.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-normalize-repeat-style@6.0.2(postcss@8.5.19): + postcss-normalize-repeat-style@6.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-normalize-string@6.0.2(postcss@8.5.19): + postcss-normalize-string@6.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-normalize-timing-functions@6.0.2(postcss@8.5.19): + postcss-normalize-timing-functions@6.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-normalize-unicode@6.1.0(postcss@8.5.19): + postcss-normalize-unicode@6.1.0(postcss@8.5.25): dependencies: browserslist: 4.28.6 - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-normalize-url@6.0.2(postcss@8.5.19): + postcss-normalize-url@6.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-normalize-whitespace@6.0.2(postcss@8.5.19): + postcss-normalize-whitespace@6.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-opacity-percentage@3.0.0(postcss@8.5.19): + postcss-opacity-percentage@3.0.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-ordered-values@6.0.2(postcss@8.5.19): + postcss-ordered-values@6.0.2(postcss@8.5.25): dependencies: - cssnano-utils: 4.0.2(postcss@8.5.19) - postcss: 8.5.19 + cssnano-utils: 4.0.2(postcss@8.5.25) + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-overflow-shorthand@6.0.0(postcss@8.5.19): + postcss-overflow-shorthand@6.0.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-page-break@3.0.4(postcss@8.5.19): + postcss-page-break@3.0.4(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-place@10.0.0(postcss@8.5.19): + postcss-place@10.0.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-preset-env@10.6.1(postcss@8.5.19): + postcss-preset-env@10.6.1(postcss@8.5.25): dependencies: - '@csstools/postcss-alpha-function': 1.0.1(postcss@8.5.19) - '@csstools/postcss-cascade-layers': 5.0.2(postcss@8.5.19) - '@csstools/postcss-color-function': 4.0.12(postcss@8.5.19) - '@csstools/postcss-color-function-display-p3-linear': 1.0.1(postcss@8.5.19) - '@csstools/postcss-color-mix-function': 3.0.12(postcss@8.5.19) - '@csstools/postcss-color-mix-variadic-function-arguments': 1.0.2(postcss@8.5.19) - '@csstools/postcss-content-alt-text': 2.0.8(postcss@8.5.19) - '@csstools/postcss-contrast-color-function': 2.0.12(postcss@8.5.19) - '@csstools/postcss-exponential-functions': 2.0.9(postcss@8.5.19) - '@csstools/postcss-font-format-keywords': 4.0.0(postcss@8.5.19) - '@csstools/postcss-gamut-mapping': 2.0.11(postcss@8.5.19) - '@csstools/postcss-gradients-interpolation-method': 5.0.12(postcss@8.5.19) - '@csstools/postcss-hwb-function': 4.0.12(postcss@8.5.19) - '@csstools/postcss-ic-unit': 4.0.4(postcss@8.5.19) - '@csstools/postcss-initial': 2.0.1(postcss@8.5.19) - '@csstools/postcss-is-pseudo-class': 5.0.3(postcss@8.5.19) - '@csstools/postcss-light-dark-function': 2.0.11(postcss@8.5.19) - '@csstools/postcss-logical-float-and-clear': 3.0.0(postcss@8.5.19) - '@csstools/postcss-logical-overflow': 2.0.0(postcss@8.5.19) - '@csstools/postcss-logical-overscroll-behavior': 2.0.0(postcss@8.5.19) - '@csstools/postcss-logical-resize': 3.0.0(postcss@8.5.19) - '@csstools/postcss-logical-viewport-units': 3.0.4(postcss@8.5.19) - '@csstools/postcss-media-minmax': 2.0.9(postcss@8.5.19) - '@csstools/postcss-media-queries-aspect-ratio-number-values': 3.0.5(postcss@8.5.19) - '@csstools/postcss-nested-calc': 4.0.0(postcss@8.5.19) - '@csstools/postcss-normalize-display-values': 4.0.1(postcss@8.5.19) - '@csstools/postcss-oklab-function': 4.0.12(postcss@8.5.19) - '@csstools/postcss-position-area-property': 1.0.0(postcss@8.5.19) - '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.19) - '@csstools/postcss-property-rule-prelude-list': 1.0.0(postcss@8.5.19) - '@csstools/postcss-random-function': 2.0.1(postcss@8.5.19) - '@csstools/postcss-relative-color-syntax': 3.0.12(postcss@8.5.19) - '@csstools/postcss-scope-pseudo-class': 4.0.1(postcss@8.5.19) - '@csstools/postcss-sign-functions': 1.1.4(postcss@8.5.19) - '@csstools/postcss-stepped-value-functions': 4.0.9(postcss@8.5.19) - '@csstools/postcss-syntax-descriptor-syntax-production': 1.0.1(postcss@8.5.19) - '@csstools/postcss-system-ui-font-family': 1.0.0(postcss@8.5.19) - '@csstools/postcss-text-decoration-shorthand': 4.0.3(postcss@8.5.19) - '@csstools/postcss-trigonometric-functions': 4.0.9(postcss@8.5.19) - '@csstools/postcss-unset-value': 4.0.0(postcss@8.5.19) - autoprefixer: 10.5.4(postcss@8.5.19) + '@csstools/postcss-alpha-function': 1.0.1(postcss@8.5.25) + '@csstools/postcss-cascade-layers': 5.0.2(postcss@8.5.25) + '@csstools/postcss-color-function': 4.0.12(postcss@8.5.25) + '@csstools/postcss-color-function-display-p3-linear': 1.0.1(postcss@8.5.25) + '@csstools/postcss-color-mix-function': 3.0.12(postcss@8.5.25) + '@csstools/postcss-color-mix-variadic-function-arguments': 1.0.2(postcss@8.5.25) + '@csstools/postcss-content-alt-text': 2.0.8(postcss@8.5.25) + '@csstools/postcss-contrast-color-function': 2.0.12(postcss@8.5.25) + '@csstools/postcss-exponential-functions': 2.0.9(postcss@8.5.25) + '@csstools/postcss-font-format-keywords': 4.0.0(postcss@8.5.25) + '@csstools/postcss-gamut-mapping': 2.0.11(postcss@8.5.25) + '@csstools/postcss-gradients-interpolation-method': 5.0.12(postcss@8.5.25) + '@csstools/postcss-hwb-function': 4.0.12(postcss@8.5.25) + '@csstools/postcss-ic-unit': 4.0.4(postcss@8.5.25) + '@csstools/postcss-initial': 2.0.1(postcss@8.5.25) + '@csstools/postcss-is-pseudo-class': 5.0.3(postcss@8.5.25) + '@csstools/postcss-light-dark-function': 2.0.11(postcss@8.5.25) + '@csstools/postcss-logical-float-and-clear': 3.0.0(postcss@8.5.25) + '@csstools/postcss-logical-overflow': 2.0.0(postcss@8.5.25) + '@csstools/postcss-logical-overscroll-behavior': 2.0.0(postcss@8.5.25) + '@csstools/postcss-logical-resize': 3.0.0(postcss@8.5.25) + '@csstools/postcss-logical-viewport-units': 3.0.4(postcss@8.5.25) + '@csstools/postcss-media-minmax': 2.0.9(postcss@8.5.25) + '@csstools/postcss-media-queries-aspect-ratio-number-values': 3.0.5(postcss@8.5.25) + '@csstools/postcss-nested-calc': 4.0.0(postcss@8.5.25) + '@csstools/postcss-normalize-display-values': 4.0.1(postcss@8.5.25) + '@csstools/postcss-oklab-function': 4.0.12(postcss@8.5.25) + '@csstools/postcss-position-area-property': 1.0.0(postcss@8.5.25) + '@csstools/postcss-progressive-custom-properties': 4.2.1(postcss@8.5.25) + '@csstools/postcss-property-rule-prelude-list': 1.0.0(postcss@8.5.25) + '@csstools/postcss-random-function': 2.0.1(postcss@8.5.25) + '@csstools/postcss-relative-color-syntax': 3.0.12(postcss@8.5.25) + '@csstools/postcss-scope-pseudo-class': 4.0.1(postcss@8.5.25) + '@csstools/postcss-sign-functions': 1.1.4(postcss@8.5.25) + '@csstools/postcss-stepped-value-functions': 4.0.9(postcss@8.5.25) + '@csstools/postcss-syntax-descriptor-syntax-production': 1.0.1(postcss@8.5.25) + '@csstools/postcss-system-ui-font-family': 1.0.0(postcss@8.5.25) + '@csstools/postcss-text-decoration-shorthand': 4.0.3(postcss@8.5.25) + '@csstools/postcss-trigonometric-functions': 4.0.9(postcss@8.5.25) + '@csstools/postcss-unset-value': 4.0.0(postcss@8.5.25) + autoprefixer: 10.5.4(postcss@8.5.25) browserslist: 4.28.6 - css-blank-pseudo: 7.0.1(postcss@8.5.19) - css-has-pseudo: 7.0.3(postcss@8.5.19) - css-prefers-color-scheme: 10.0.0(postcss@8.5.19) + css-blank-pseudo: 7.0.1(postcss@8.5.25) + css-has-pseudo: 7.0.3(postcss@8.5.25) + css-prefers-color-scheme: 10.0.0(postcss@8.5.25) cssdb: 8.9.0 - postcss: 8.5.19 - postcss-attribute-case-insensitive: 7.0.1(postcss@8.5.19) - postcss-clamp: 4.1.0(postcss@8.5.19) - postcss-color-functional-notation: 7.0.12(postcss@8.5.19) - postcss-color-hex-alpha: 10.0.0(postcss@8.5.19) - postcss-color-rebeccapurple: 10.0.0(postcss@8.5.19) - postcss-custom-media: 11.0.6(postcss@8.5.19) - postcss-custom-properties: 14.0.6(postcss@8.5.19) - postcss-custom-selectors: 8.0.5(postcss@8.5.19) - postcss-dir-pseudo-class: 9.0.1(postcss@8.5.19) - postcss-double-position-gradients: 6.0.4(postcss@8.5.19) - postcss-focus-visible: 10.0.1(postcss@8.5.19) - postcss-focus-within: 9.0.1(postcss@8.5.19) - postcss-font-variant: 5.0.0(postcss@8.5.19) - postcss-gap-properties: 6.0.0(postcss@8.5.19) - postcss-image-set-function: 7.0.0(postcss@8.5.19) - postcss-lab-function: 7.0.12(postcss@8.5.19) - postcss-logical: 8.1.0(postcss@8.5.19) - postcss-nesting: 13.0.2(postcss@8.5.19) - postcss-opacity-percentage: 3.0.0(postcss@8.5.19) - postcss-overflow-shorthand: 6.0.0(postcss@8.5.19) - postcss-page-break: 3.0.4(postcss@8.5.19) - postcss-place: 10.0.0(postcss@8.5.19) - postcss-pseudo-class-any-link: 10.0.1(postcss@8.5.19) - postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.19) - postcss-selector-not: 8.0.1(postcss@8.5.19) + postcss: 8.5.25 + postcss-attribute-case-insensitive: 7.0.1(postcss@8.5.25) + postcss-clamp: 4.1.0(postcss@8.5.25) + postcss-color-functional-notation: 7.0.12(postcss@8.5.25) + postcss-color-hex-alpha: 10.0.0(postcss@8.5.25) + postcss-color-rebeccapurple: 10.0.0(postcss@8.5.25) + postcss-custom-media: 11.0.6(postcss@8.5.25) + postcss-custom-properties: 14.0.6(postcss@8.5.25) + postcss-custom-selectors: 8.0.5(postcss@8.5.25) + postcss-dir-pseudo-class: 9.0.1(postcss@8.5.25) + postcss-double-position-gradients: 6.0.4(postcss@8.5.25) + postcss-focus-visible: 10.0.1(postcss@8.5.25) + postcss-focus-within: 9.0.1(postcss@8.5.25) + postcss-font-variant: 5.0.0(postcss@8.5.25) + postcss-gap-properties: 6.0.0(postcss@8.5.25) + postcss-image-set-function: 7.0.0(postcss@8.5.25) + postcss-lab-function: 7.0.12(postcss@8.5.25) + postcss-logical: 8.1.0(postcss@8.5.25) + postcss-nesting: 13.0.2(postcss@8.5.25) + postcss-opacity-percentage: 3.0.0(postcss@8.5.25) + postcss-overflow-shorthand: 6.0.0(postcss@8.5.25) + postcss-page-break: 3.0.4(postcss@8.5.25) + postcss-place: 10.0.0(postcss@8.5.25) + postcss-pseudo-class-any-link: 10.0.1(postcss@8.5.25) + postcss-replace-overflow-wrap: 4.0.0(postcss@8.5.25) + postcss-selector-not: 8.0.1(postcss@8.5.25) - postcss-pseudo-class-any-link@10.0.1(postcss@8.5.19): + postcss-pseudo-class-any-link@10.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 - postcss-reduce-idents@6.0.3(postcss@8.5.19): + postcss-reduce-idents@6.0.3(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-reduce-initial@6.1.0(postcss@8.5.19): + postcss-reduce-initial@6.1.0(postcss@8.5.25): dependencies: browserslist: 4.28.6 caniuse-api: 3.0.0 - postcss: 8.5.19 + postcss: 8.5.25 - postcss-reduce-transforms@6.0.2(postcss@8.5.19): + postcss-reduce-transforms@6.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 - postcss-replace-overflow-wrap@4.0.0(postcss@8.5.19): + postcss-replace-overflow-wrap@4.0.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-safe-parser@7.0.1(postcss@8.5.19): + postcss-safe-parser@7.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-scss@4.0.9(postcss@8.5.19): + postcss-scss@4.0.9(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss-selector-not@8.0.1(postcss@8.5.19): + postcss-selector-not@8.0.1(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 7.1.4 postcss-selector-parser@6.1.4: @@ -24423,29 +24336,29 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-sort-media-queries@5.2.0(postcss@8.5.19): + postcss-sort-media-queries@5.2.0(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 sort-css-media-queries: 2.2.0 - postcss-svgo@6.0.3(postcss@8.5.19): + postcss-svgo@6.0.3(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-value-parser: 4.2.0 svgo: 3.3.4 - postcss-unique-selectors@6.0.4(postcss@8.5.19): + postcss-unique-selectors@6.0.4(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 6.1.4 postcss-value-parser@4.2.0: {} - postcss-zindex@6.0.2(postcss@8.5.19): + postcss-zindex@6.0.2(postcss@8.5.25): dependencies: - postcss: 8.5.19 + postcss: 8.5.25 - postcss@8.5.19: + postcss@8.5.25: dependencies: nanoid: 3.3.16 picocolors: 1.1.1 @@ -24489,21 +24402,21 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier-plugin-organize-imports@4.3.0(@typescript/typescript6@6.0.2)(prettier@3.9.5): + prettier-plugin-organize-imports@4.3.0(@typescript/typescript6@6.0.2)(prettier@3.9.6): dependencies: - prettier: 3.9.5 + prettier: 3.9.6 typescript: '@typescript/typescript6@6.0.2' - prettier-plugin-sort-json@4.2.0(prettier@3.9.5): + prettier-plugin-sort-json@4.2.0(prettier@3.9.6): dependencies: - prettier: 3.9.5 + prettier: 3.9.6 - prettier-plugin-svelte@4.1.1(prettier@3.9.5)(svelte@5.56.5(@typescript-eslint/types@8.64.0)): + prettier-plugin-svelte@4.1.1(prettier@3.9.6)(svelte@5.56.8(@typescript-eslint/types@8.65.0)): dependencies: - prettier: 3.9.5 - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + prettier: 3.9.6 + svelte: 5.56.8(@typescript-eslint/types@8.65.0) - prettier@3.9.5: {} + prettier@3.9.6: {} pretty-error@4.0.0: dependencies: @@ -24518,11 +24431,11 @@ snapshots: pretty-time@1.1.0: {} - prism-react-renderer@2.4.1(react@19.2.7): + prism-react-renderer@2.4.1(react@19.2.8): dependencies: '@types/prismjs': 1.26.6 clsx: 2.1.1 - react: 19.2.7 + react: 19.2.8 prismjs@1.30.0: {} @@ -24549,9 +24462,9 @@ snapshots: retry: 0.12.0 signal-exit: 3.0.7 - properties-reader@3.0.1: + properties-reader@3.0.1(supports-color@8.1.1): dependencies: - '@kwsites/file-exists': 1.1.1 + '@kwsites/file-exists': 1.1.1(supports-color@8.1.1) mkdirp: 3.0.1 transitivePeerDependencies: - supports-color @@ -24658,11 +24571,16 @@ snapshots: iconv-lite: 0.7.3 unpipe: 1.0.0 - raw-loader@4.0.2(webpack@5.108.4(postcss@8.5.19)): + raw-body@4.0.0: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + + raw-loader@4.0.2(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.108.4(postcss@8.5.19) + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) rc@1.2.8: dependencies: @@ -24671,15 +24589,15 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 - react-dom@19.2.7(react@19.2.7): + react-dom@19.2.8(react@19.2.8): dependencies: - react: 19.2.7 + react: 19.2.8 scheduler: 0.27.0 - react-email@5.2.11: + react-email@5.2.11(supports-color@8.1.1): dependencies: '@babel/parser': 7.27.0 - '@babel/traverse': 7.27.0 + '@babel/traverse': 7.27.0(supports-color@8.1.1) chokidar: 4.0.3 commander: 13.1.0 conf: 15.1.0 @@ -24693,7 +24611,7 @@ snapshots: nypm: 0.6.5 ora: 8.2.0 prompts: 2.4.2 - socket.io: 4.8.3 + socket.io: 4.8.3(supports-color@8.1.1) tsconfig-paths: 4.2.0 transitivePeerDependencies: - bufferutil @@ -24706,34 +24624,34 @@ snapshots: react-is@17.0.2: {} - react-json-view-lite@2.5.0(react@19.2.7): + react-json-view-lite@2.5.0(react@19.2.8): dependencies: - react: 19.2.7 + react: 19.2.8 - react-loadable-ssr-addon-v5-slorber@1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.7))(webpack@5.108.4(postcss@8.5.19)): + react-loadable-ssr-addon-v5-slorber@1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.8))(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): dependencies: '@babel/runtime': 7.29.7 - react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.7)' - webpack: 5.108.4(postcss@8.5.19) + react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.8)' + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) - react-router-config@5.1.1(react-router@5.3.4(react@19.2.7))(react@19.2.7): + react-router-config@5.1.1(react-router@5.3.4(react@19.2.8))(react@19.2.8): dependencies: '@babel/runtime': 7.29.7 - react: 19.2.7 - react-router: 5.3.4(react@19.2.7) + react: 19.2.8 + react-router: 5.3.4(react@19.2.8) - react-router-dom@5.3.4(react@19.2.7): + react-router-dom@5.3.4(react@19.2.8): dependencies: '@babel/runtime': 7.29.7 history: 4.10.1 loose-envify: 1.4.0 prop-types: 15.8.1 - react: 19.2.7 - react-router: 5.3.4(react@19.2.7) + react: 19.2.8 + react-router: 5.3.4(react@19.2.8) tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - react-router@5.3.4(react@19.2.7): + react-router@5.3.4(react@19.2.8): dependencies: '@babel/runtime': 7.29.7 history: 4.10.1 @@ -24741,12 +24659,12 @@ snapshots: loose-envify: 1.4.0 path-to-regexp: 1.9.0 prop-types: 15.8.1 - react: 19.2.7 + react: 19.2.8 react-is: 16.13.1 tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - react@19.2.7: {} + react@19.2.8: {} read-cache@1.0.0: dependencies: @@ -24794,10 +24712,10 @@ snapshots: estree-util-build-jsx: 3.0.1 vfile: 6.0.3 - recma-jsx@1.0.1(acorn@8.17.0): + recma-jsx@1.0.1(acorn@8.18.0): dependencies: - acorn: 8.17.0 - acorn-jsx: 5.3.2(acorn@8.17.0) + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) estree-util-to-js: 2.0.0 recma-parse: 1.0.0 recma-stringify: 1.0.0 @@ -24870,20 +24788,20 @@ snapshots: hast-util-raw: 9.1.0 vfile: 6.0.3 - rehype-recma@1.0.0: + rehype-recma@1.0.0(supports-color@8.1.1): dependencies: '@types/estree': 1.0.9 '@types/hast': 3.0.5 - hast-util-to-estree: 3.1.3 + hast-util-to-estree: 3.1.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color relateurl@0.2.7: {} - remark-directive@3.0.1: + remark-directive@3.0.1(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 - mdast-util-directive: 3.1.0 + mdast-util-directive: 3.1.0(supports-color@8.1.1) micromark-extension-directive: 3.0.2 unified: 11.0.5 transitivePeerDependencies: @@ -24897,37 +24815,37 @@ snapshots: node-emoji: 2.2.0 unified: 11.0.5 - remark-frontmatter@5.0.0: + remark-frontmatter@5.0.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 - mdast-util-frontmatter: 2.0.1 + mdast-util-frontmatter: 2.0.1(supports-color@8.1.1) micromark-extension-frontmatter: 2.0.0 unified: 11.0.5 transitivePeerDependencies: - supports-color - remark-gfm@4.0.1: + remark-gfm@4.0.1(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 - mdast-util-gfm: 3.1.0 + mdast-util-gfm: 3.1.0(supports-color@8.1.1) micromark-extension-gfm: 3.0.0 - remark-parse: 11.0.0 + remark-parse: 11.0.0(supports-color@8.1.1) remark-stringify: 11.0.0 unified: 11.0.5 transitivePeerDependencies: - supports-color - remark-mdx@3.1.1: + remark-mdx@3.1.1(supports-color@8.1.1): dependencies: - mdast-util-mdx: 3.0.0 + mdast-util-mdx: 3.0.0(supports-color@8.1.1) micromark-extension-mdxjs: 3.0.0 transitivePeerDependencies: - supports-color - remark-parse@11.0.0: + remark-parse@11.0.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) micromark-util-types: 2.0.2 unified: 11.0.5 transitivePeerDependencies: @@ -24961,9 +24879,9 @@ snapshots: require-from-string@2.0.2: {} - require-in-the-middle@8.0.1: + require-in-the-middle@8.0.1(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) module-details-from-path: 1.0.4 transitivePeerDependencies: - supports-color @@ -25092,9 +25010,9 @@ snapshots: points-on-curve: 0.2.0 points-on-path: 0.2.1 - router@2.2.0: + router@2.2.0(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -25109,7 +25027,7 @@ snapshots: dependencies: escalade: 3.2.0 picocolors: 1.1.1 - postcss: 8.5.19 + postcss: 8.5.25 strip-json-comments: 3.1.1 run-applescript@7.1.0: {} @@ -25120,14 +25038,14 @@ snapshots: dependencies: queue-microtask: 1.2.3 - runed@0.35.1(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.5(@typescript-eslint/types@8.64.0)): + runed@0.35.1(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)): dependencies: dequal: 2.0.3 esm-env: 1.2.2 lz-string: 1.5.0 - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) optionalDependencies: - '@sveltejs/kit': 2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + '@sveltejs/kit': 2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) rw@1.3.3: {} @@ -25153,7 +25071,7 @@ snapshots: dependencies: truncate-utf8-bytes: 1.0.2 - sass@1.101.0: + sass@1.102.0: dependencies: chokidar: 5.0.0 immutable: 5.1.6 @@ -25211,9 +25129,9 @@ snapshots: semver@7.8.5: {} - send@0.19.2: + send@0.19.2(supports-color@8.1.1): dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) depd: 2.0.0 destroy: 1.2.0 encodeurl: 2.0.0 @@ -25229,9 +25147,9 @@ snapshots: transitivePeerDependencies: - supports-color - send@1.2.1: + send@1.2.1(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -25259,11 +25177,11 @@ snapshots: path-to-regexp: 3.3.0 range-parser: 1.2.0 - serve-index@1.9.2: + serve-index@1.9.2(supports-color@8.1.1): dependencies: accepts: 1.3.8 batch: 0.6.1 - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) escape-html: 1.0.3 http-errors: 1.8.1 mime-types: 2.1.35 @@ -25271,21 +25189,21 @@ snapshots: transitivePeerDependencies: - supports-color - serve-static@1.16.3: + serve-static@1.16.3(supports-color@8.1.1): dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 0.19.2 + send: 0.19.2(supports-color@8.1.1) transitivePeerDependencies: - supports-color - serve-static@2.2.1: + serve-static@2.2.1(supports-color@8.1.1): dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 1.2.1 + send: 1.2.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -25404,7 +25322,7 @@ snapshots: simple-concat: 1.0.1 optional: true - simple-icons@16.26.0: {} + simple-icons@16.27.1: {} sirv@2.0.4: dependencies: @@ -25442,42 +25360,42 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 - socket.io-adapter@2.5.7: + socket.io-adapter@2.5.7(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) ws: 8.20.1 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - socket.io-client@4.8.3: + socket.io-client@4.8.3(supports-color@8.1.1): dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.4.3 - engine.io-client: 6.6.5 - socket.io-parser: 4.2.6 + debug: 4.4.3(supports-color@8.1.1) + engine.io-client: 6.6.5(supports-color@8.1.1) + socket.io-parser: 4.2.6(supports-color@8.1.1) transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - socket.io-parser@4.2.6: + socket.io-parser@4.2.6(supports-color@8.1.1): dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color - socket.io@4.8.3: + socket.io@4.8.3(supports-color@8.1.1): dependencies: accepts: 1.3.8 base64id: 2.0.0 cors: 2.8.6 - debug: 4.4.3 - engine.io: 6.6.8 - socket.io-adapter: 2.5.7 - socket.io-parser: 4.2.6 + debug: 4.4.3(supports-color@8.1.1) + engine.io: 6.6.8(supports-color@8.1.1) + socket.io-adapter: 2.5.7(supports-color@8.1.1) + socket.io-parser: 4.2.6(supports-color@8.1.1) transitivePeerDependencies: - bufferutil - supports-color @@ -25508,9 +25426,9 @@ snapshots: space-separated-tokens@2.0.2: {} - spdy-transport@3.0.0: + spdy-transport@3.0.0(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) detect-node: 2.1.0 hpack.js: 2.1.6 obuf: 1.1.2 @@ -25519,13 +25437,13 @@ snapshots: transitivePeerDependencies: - supports-color - spdy@4.0.2: + spdy@4.0.2(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) handle-thing: 2.0.1 http-deceiver: 1.2.7 select-hose: 2.0.0 - spdy-transport: 3.0.0 + spdy-transport: 3.0.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -25665,10 +25583,10 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - stylehacks@6.1.1(postcss@8.5.19): + stylehacks@6.1.1(postcss@8.5.25): dependencies: browserslist: 4.28.6 - postcss: 8.5.19 + postcss: 8.5.25 postcss-selector-parser: 6.1.4 stylis@4.4.0: {} @@ -25689,11 +25607,11 @@ snapshots: make-asynchronous: 1.1.0 time-span: 5.1.0 - superagent@10.3.0: + superagent@10.3.0(supports-color@8.1.1): dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) fast-safe-stringify: 2.1.1 form-data: 4.0.6 formidable: 3.5.4 @@ -25703,11 +25621,11 @@ snapshots: transitivePeerDependencies: - supports-color - supertest@7.2.2: + supertest@7.2.2(supports-color@8.1.1): dependencies: cookie-signature: 1.2.2 methods: 1.1.2 - superagent: 10.3.0 + superagent: 10.3.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -25721,11 +25639,11 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-awesome@3.3.5(svelte@5.56.5(@typescript-eslint/types@8.64.0)): + svelte-awesome@3.3.5(svelte@5.56.8(@typescript-eslint/types@8.65.0)): dependencies: - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) - svelte-check@4.7.3(@typescript/typescript6@6.0.2)(picomatch@4.0.5)(svelte@5.56.5(@typescript-eslint/types@8.64.0)): + svelte-check@4.7.3(@typescript/typescript6@6.0.2)(picomatch@4.0.5)(svelte@5.56.8(@typescript-eslint/types@8.65.0)): dependencies: '@jridgewell/trace-mapping': 0.3.31 '@sveltejs/load-config': 0.2.0 @@ -25733,27 +25651,27 @@ snapshots: fdir: 6.5.0(picomatch@4.0.5) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - picomatch - svelte-eslint-parser@1.8.0(svelte@5.56.5(@typescript-eslint/types@8.64.0)): + svelte-eslint-parser@1.8.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)): dependencies: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 espree: 10.4.0 - postcss: 8.5.19 - postcss-scss: 4.0.9(postcss@8.5.19) + postcss: 8.5.25 + postcss-scss: 4.0.9(postcss@8.5.25) postcss-selector-parser: 7.1.4 semver: 7.8.5 optionalDependencies: - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) svelte-floating-ui@1.5.8: dependencies: - '@floating-ui/core': 1.7.5 - '@floating-ui/dom': 1.7.6 + '@floating-ui/core': 1.8.0 + '@floating-ui/dom': 1.8.0 svelte-gestures@5.2.2: {} @@ -25761,7 +25679,7 @@ snapshots: dependencies: highlight.js: 11.11.1 - svelte-i18n@4.0.1(svelte@5.56.5(@typescript-eslint/types@8.64.0)): + svelte-i18n@4.0.1(svelte@5.56.8(@typescript-eslint/types@8.65.0)): dependencies: cli-color: 2.0.4 deepmerge: 4.3.1 @@ -25769,85 +25687,85 @@ snapshots: estree-walker: 2.0.2 intl-messageformat: 10.7.18 sade: 1.8.1 - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) tiny-glob: 0.2.9 - svelte-jsoneditor@3.12.0(svelte@5.56.5(@typescript-eslint/types@8.64.0)): + svelte-jsoneditor@3.13.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)): dependencies: '@codemirror/autocomplete': 6.20.3 - '@codemirror/commands': 6.10.3 + '@codemirror/commands': 6.10.4 '@codemirror/lang-json': 6.0.2 - '@codemirror/language': 6.12.3 + '@codemirror/language': 6.12.4 '@codemirror/lint': 6.9.7 - '@codemirror/search': 6.7.0 - '@codemirror/state': 6.6.0 - '@codemirror/view': 6.43.1 - '@fortawesome/free-regular-svg-icons': 7.2.0 - '@fortawesome/free-solid-svg-icons': 7.2.0 + '@codemirror/search': 6.7.1 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.7 + '@fortawesome/free-regular-svg-icons': 7.3.1 + '@fortawesome/free-solid-svg-icons': 7.3.1 '@jsonquerylang/jsonquery': 5.1.1 '@lezer/highlight': 1.2.3 - '@replit/codemirror-indentation-markers': 6.5.3(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.1) + '@replit/codemirror-indentation-markers': 6.5.3(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.7) ajv: 8.20.0 - codemirror-wrapped-line-indent: 1.0.9(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.1) + codemirror-wrapped-line-indent: 1.0.9(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.7) diff-sequences: 29.6.3 - immutable-json-patch: 6.0.2 + immutable-json-patch: 6.0.3 jmespath: 0.16.0 json-source-map: 0.6.1 jsonpath-plus: 10.4.0 - jsonrepair: 3.14.0 + jsonrepair: 3.15.0 lodash-es: 4.18.1 memoize-one: 6.0.0 natural-compare-lite: 1.4.0 - sass: 1.101.0 - svelte: 5.56.5(@typescript-eslint/types@8.64.0) - svelte-awesome: 3.3.5(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + sass: 1.102.0 + svelte: 5.56.8(@typescript-eslint/types@8.65.0) + svelte-awesome: 3.3.5(svelte@5.56.8(@typescript-eslint/types@8.65.0)) svelte-select: 5.8.3 vanilla-picker: 2.12.3 - svelte-maplibre@1.3.0(svelte@5.56.5(@typescript-eslint/types@8.64.0)): + svelte-maplibre@1.3.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)): dependencies: d3-geo: 3.1.1 dequal: 2.0.3 just-compare: 2.3.0 maplibre-gl: 5.24.0 pmtiles: 3.2.1 - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) - svelte-parse-markup@0.1.5(svelte@5.56.5(@typescript-eslint/types@8.64.0)): + svelte-parse-markup@0.1.5(svelte@5.56.8(@typescript-eslint/types@8.65.0)): dependencies: - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) - svelte-persisted-store@0.12.0(svelte@5.56.5(@typescript-eslint/types@8.64.0)): + svelte-persisted-store@0.12.0(svelte@5.56.8(@typescript-eslint/types@8.65.0)): dependencies: - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) svelte-select@5.8.3: dependencies: svelte-floating-ui: 1.5.8 - svelte-toolbelt@0.10.6(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.5(@typescript-eslint/types@8.64.0)): + svelte-toolbelt@0.10.6(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)): dependencies: clsx: 2.1.1 - runed: 0.35.1(@sveltejs/kit@2.69.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.5(@typescript-eslint/types@8.64.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.5(@typescript-eslint/types@8.64.0)) + runed: 0.35.1(@sveltejs/kit@2.70.1(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.2.0(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(@typescript/typescript6@6.0.2)(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)))(svelte@5.56.8(@typescript-eslint/types@8.65.0)) style-to-object: 1.0.14 - svelte: 5.56.5(@typescript-eslint/types@8.64.0) + svelte: 5.56.8(@typescript-eslint/types@8.65.0) transitivePeerDependencies: - '@sveltejs/kit' - svelte@5.56.5(@typescript-eslint/types@8.64.0): + svelte@5.56.8(@typescript-eslint/types@8.65.0): dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.11(acorn@8.17.0) + '@sveltejs/acorn-typescript': 1.0.11(acorn@8.18.0) '@types/estree': 1.0.9 '@types/trusted-types': 2.0.7 - acorn: 8.17.0 + acorn: 8.18.0 aria-query: 5.3.1 axobject-query: 4.1.0 clsx: 2.1.1 - devalue: 5.8.1 + devalue: 5.8.2 esm-env: 1.2.2 - esrap: 2.2.13(@typescript-eslint/types@8.64.0) + esrap: 2.2.13(@typescript-eslint/types@8.65.0) is-reference: 3.0.3 locate-character: 3.0.0 magic-string: 0.30.21 @@ -25890,9 +25808,9 @@ snapshots: tailwind-merge@3.6.0: {} - tailwind-variants@3.2.2(tailwind-merge@3.6.0)(tailwindcss@4.3.2): + tailwind-variants@3.2.2(tailwind-merge@3.6.0)(tailwindcss@4.3.3): dependencies: - tailwindcss: 4.3.2 + tailwindcss: 4.3.3 optionalDependencies: tailwind-merge: 3.6.0 @@ -25926,11 +25844,11 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.19 - postcss-import: 15.1.0(postcss@8.5.19) - postcss-js: 4.1.0(postcss@8.5.19) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.19)(tsx@4.23.1)(yaml@2.9.0) - postcss-nested: 6.2.0(postcss@8.5.19) + postcss: 8.5.25 + postcss-import: 15.1.0(postcss@8.5.25) + postcss-js: 4.1.0(postcss@8.5.25) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.25)(tsx@4.23.1)(yaml@2.9.0) + postcss-nested: 6.2.0(postcss@8.5.25) postcss-selector-parser: 6.1.4 resolve: 1.22.12 sucrase: 3.35.1 @@ -25938,7 +25856,7 @@ snapshots: - tsx - yaml - tailwindcss@4.3.2: {} + tailwindcss@4.3.3: {} tapable@2.3.3: {} @@ -26004,51 +25922,62 @@ snapshots: - bare-abort-controller - react-native-b4a - terser-webpack-plugin@5.6.1(@swc/core@1.15.43(@swc/helpers@0.5.23))(esbuild@0.28.1)(lightningcss@1.33.0)(webpack@5.106.2(@swc/core@1.15.43(@swc/helpers@0.5.23))(esbuild@0.28.1)(lightningcss@1.33.0)): + terser-webpack-plugin@5.6.1(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)(webpack@5.106.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.49.0 - webpack: 5.106.2(@swc/core@1.15.43(@swc/helpers@0.5.23))(esbuild@0.28.1)(lightningcss@1.33.0) - optionalDependencies: - '@swc/core': 1.15.43(@swc/helpers@0.5.23) - esbuild: 0.28.1 - lightningcss: 1.33.0 - - terser-webpack-plugin@5.6.1(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(webpack@5.108.4(postcss@8.5.19)): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - jest-worker: 27.5.1 - schema-utils: 4.3.3 - terser: 5.49.0 - webpack: 5.108.4(postcss@8.5.19) + webpack: 5.106.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) optionalDependencies: + '@swc/core': 1.15.46(@swc/helpers@0.5.23) clean-css: 5.3.3 - cssnano: 6.1.2(postcss@8.5.19) + cssnano: 6.1.2(postcss@8.5.25) + csso: 5.0.5 + esbuild: 0.28.1 html-minifier-terser: 7.2.0 - postcss: 8.5.19 + lightningcss: 1.33.0 + postcss: 8.5.25 + uglify-js: 3.19.3 + + terser-webpack-plugin@5.6.1(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.49.0 + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) + optionalDependencies: + '@swc/core': 1.15.46(@swc/helpers@0.5.23) + clean-css: 5.3.3 + cssnano: 6.1.2(postcss@8.5.25) + csso: 5.0.5 + esbuild: 0.28.1 + html-minifier-terser: 7.2.0 + lightningcss: 1.33.0 + postcss: 8.5.25 + uglify-js: 3.19.3 terser@5.49.0: dependencies: '@jridgewell/source-map': 0.3.11 - acorn: 8.17.0 + acorn: 8.18.0 commander: 2.20.3 source-map-support: 0.5.21 - testcontainers@12.0.4: + testcontainers@12.0.4(supports-color@8.1.1): dependencies: '@balena/dockerignore': 1.0.2 '@types/dockerode': 4.0.1 archiver: 7.0.1 async-lock: 1.4.1 byline: 5.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) docker-compose: 1.4.2 - dockerode: 5.0.1 + dockerode: 5.0.1(supports-color@8.1.1) get-port: 5.1.1 proper-lockfile: 4.1.2 - properties-reader: 3.0.1 + properties-reader: 3.0.1(supports-color@8.1.1) ssh-remote-port-forward: 1.0.4 tar-fs: 3.1.3 tmp: 0.2.7 @@ -26079,8 +26008,6 @@ snapshots: dependencies: tslib: 2.8.1 - three@0.184.0: {} - three@0.185.1: {} through@2.3.8: {} @@ -26210,7 +26137,7 @@ snapshots: tsconfig-paths-webpack-plugin@4.2.0: dependencies: chalk: 4.1.2 - enhanced-resolve: 5.24.2 + enhanced-resolve: 5.24.4 tapable: 2.3.3 tsconfig-paths: 4.2.0 @@ -26278,13 +26205,13 @@ snapshots: typedarray@0.0.6: {} - typescript-eslint@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)): + typescript-eslint@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@typescript-eslint/eslint-plugin': 8.64.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)))(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) - '@typescript-eslint/parser': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) - '@typescript-eslint/typescript-estree': 8.64.0(@typescript/typescript6@6.0.2) - '@typescript-eslint/utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(jiti@2.7.0)) - eslint: 10.7.0(jiti@2.7.0) + '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1))(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/parser': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/typescript-estree': 8.65.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@typescript-eslint/utils': 8.65.0(@typescript/typescript6@6.0.2)(eslint@10.8.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1) + eslint: 10.8.0(jiti@2.7.0)(supports-color@8.1.1) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color @@ -26432,10 +26359,10 @@ snapshots: unpipe@1.0.0: {} - unplugin-swc@1.5.9(@swc/core@1.15.43(@swc/helpers@0.5.23))(rollup@4.62.0): + unplugin-swc@1.5.9(@swc/core@1.15.46(@swc/helpers@0.5.23))(rollup@4.62.0): dependencies: '@rollup/pluginutils': 5.4.0(rollup@4.62.0) - '@swc/core': 1.15.43(@swc/helpers@0.5.23) + '@swc/core': 1.15.46(@swc/helpers@0.5.23) load-tsconfig: 0.2.5 unplugin: 2.3.11 transitivePeerDependencies: @@ -26444,7 +26371,7 @@ snapshots: unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 - acorn: 8.17.0 + acorn: 8.18.0 picomatch: 4.0.5 webpack-virtual-modules: 0.6.2 @@ -26477,14 +26404,14 @@ snapshots: dependencies: punycode: 2.3.1 - url-loader@4.1.1(file-loader@6.2.0(webpack@5.108.4(postcss@8.5.19)))(webpack@5.108.4(postcss@8.5.19)): + url-loader@4.1.1(file-loader@6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)))(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): dependencies: loader-utils: 2.0.4 mime-types: 2.1.35 schema-utils: 3.3.0 - webpack: 5.108.4(postcss@8.5.19) + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) optionalDependencies: - file-loader: 6.2.0(webpack@5.108.4(postcss@8.5.19)) + file-loader: 6.2.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) url@0.11.4: dependencies: @@ -26503,9 +26430,9 @@ snapshots: utils-merge@1.0.1: {} - utimes@5.2.1: + utimes@5.2.1(supports-color@8.1.1): dependencies: - '@mapbox/node-pre-gyp': 1.0.11 + '@mapbox/node-pre-gyp': 1.0.11(supports-color@8.1.1) node-addon-api: 4.3.0 transitivePeerDependencies: - encoding @@ -26566,13 +26493,13 @@ snapshots: transitivePeerDependencies: - rollup - vite-node@3.2.4(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0): + vite-node@3.2.4(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0): dependencies: cac: 6.7.14 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -26587,22 +26514,22 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@6.1.1(@typescript/typescript6@6.0.2)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)): + vite-tsconfig-paths@6.1.1(@typescript/typescript6@6.0.2)(supports-color@8.1.1)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) globrex: 0.1.2 tsconfck: 3.1.6(@typescript/typescript6@6.0.2) - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) transitivePeerDependencies: - supports-color - typescript - vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0): + vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0): dependencies: esbuild: 0.28.1 fdir: 6.5.0(picomatch@4.0.5) picomatch: 4.0.5 - postcss: 8.5.19 + postcss: 8.5.25 rollup: 4.62.0 tinyglobby: 0.2.17 optionalDependencies: @@ -26610,16 +26537,16 @@ snapshots: fsevents: 2.3.3 jiti: 2.7.0 lightningcss: 1.33.0 - sass: 1.101.0 + sass: 1.102.0 terser: 5.49.0 tsx: 4.23.1 yaml: 2.9.0 - vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0): + vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 - postcss: 8.5.19 + postcss: 8.5.25 rolldown: 1.1.5 tinyglobby: 0.2.17 optionalDependencies: @@ -26627,31 +26554,31 @@ snapshots: esbuild: 0.28.1 fsevents: 2.3.3 jiti: 2.7.0 - sass: 1.101.0 + sass: 1.102.0 terser: 5.49.0 tsx: 4.23.1 yaml: 2.9.0 - vitefu@1.1.3(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)): + vitefu@1.1.3(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)): optionalDependencies: - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) vitest-fetch-mock@0.4.5(vitest@4.1.10): dependencies: - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6)(jsdom@26.1.0(canvas@3.2.3))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) - vitest@3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.10.6)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.33.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0): + vitest@3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.1)(jiti@2.7.0)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.7 - '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) '@vitest/pretty-format': 3.2.7 '@vitest/runner': 3.2.7 '@vitest/snapshot': 3.2.7 '@vitest/spy': 3.2.7 '@vitest/utils': 3.2.7 chai: 5.3.3 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) expect-type: 1.4.0 magic-string: 0.30.21 pathe: 2.0.3 @@ -26662,14 +26589,14 @@ snapshots: tinyglobby: 0.2.17 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) - vite-node: 3.2.4(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 7.3.6(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite-node: 3.2.4(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.33.0)(sass@1.102.0)(supports-color@8.1.1)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.13 '@types/node': 24.13.3 - happy-dom: 20.10.6 - jsdom: 26.1.0(canvas@3.2.3) + happy-dom: 20.11.1 + jsdom: 26.1.0(canvas@3.2.3)(supports-color@8.1.1) transitivePeerDependencies: - jiti - less @@ -26684,10 +26611,10 @@ snapshots: - tsx - yaml - vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6)(jsdom@26.1.0(canvas@3.2.3))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)): + vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3)(supports-color@8.1.1))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -26704,13 +26631,44 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 24.13.3 '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) - happy-dom: 20.10.6 + happy-dom: 20.11.1 + jsdom: 26.1.0(canvas@3.2.3)(supports-color@8.1.1) + transitivePeerDependencies: + - msw + + vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(happy-dom@20.11.1)(jsdom@26.1.0(canvas@3.2.3))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.3 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/node': 24.13.3 + '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) + happy-dom: 20.11.1 jsdom: 26.1.0(canvas@3.2.3) transitivePeerDependencies: - msw @@ -26748,7 +26706,7 @@ snapshots: webpack-bundle-analyzer@4.10.2: dependencies: '@discoveryjs/json-ext': 0.5.7 - acorn: 8.17.0 + acorn: 8.18.0 acorn-walk: 8.3.5 commander: 7.2.0 debounce: 1.2.1 @@ -26763,7 +26721,7 @@ snapshots: - bufferutil - utf-8-validate - webpack-dev-middleware@7.4.5(tslib@2.8.1)(webpack@5.108.4(postcss@8.5.19)): + webpack-dev-middleware@7.4.5(tslib@2.8.1)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): dependencies: colorette: 2.0.20 memfs: 4.64.0(tslib@2.8.1) @@ -26772,11 +26730,11 @@ snapshots: range-parser: 1.3.0 schema-utils: 4.3.3 optionalDependencies: - webpack: 5.108.4(postcss@8.5.19) + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) transitivePeerDependencies: - tslib - webpack-dev-server@5.2.6(tslib@2.8.1)(webpack@5.108.4(postcss@8.5.19)): + webpack-dev-server@5.2.6(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(tslib@2.8.1)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -26790,24 +26748,24 @@ snapshots: bonjour-service: 1.4.3 chokidar: 3.6.0 colorette: 2.0.20 - compression: 1.8.1 + compression: 1.8.1(supports-color@8.1.1) connect-history-api-fallback: 2.0.0 - express: 4.22.2 + express: 4.22.2(supports-color@8.1.1) graceful-fs: 4.2.11 - http-proxy-middleware: 2.0.10(@types/express@4.17.25) + http-proxy-middleware: 2.0.10(@types/express@4.17.25)(debug@4.4.3(supports-color@8.1.1)) ipaddr.js: 2.4.0 launch-editor: 2.14.1 open: 10.2.0 p-retry: 6.2.1 schema-utils: 4.3.3 selfsigned: 5.5.0 - serve-index: 1.9.2 + serve-index: 1.9.2(supports-color@8.1.1) sockjs: 0.3.24 - spdy: 4.0.2 - webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.108.4(postcss@8.5.19)) + spdy: 4.0.2(supports-color@8.1.1) + webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) ws: 8.21.1 optionalDependencies: - webpack: 5.108.4(postcss@8.5.19) + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) transitivePeerDependencies: - bufferutil - debug @@ -26833,7 +26791,7 @@ snapshots: webpack-virtual-modules@0.6.2: {} - webpack@5.106.2(@swc/core@1.15.43(@swc/helpers@0.5.23))(esbuild@0.28.1)(lightningcss@1.33.0): + webpack@5.106.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.9 @@ -26841,11 +26799,11 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.17.0 - acorn-import-phases: 1.0.4(acorn@8.17.0) + acorn: 8.18.0 + acorn-import-phases: 1.0.4(acorn@8.18.0) browserslist: 4.28.6 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.24.2 + enhanced-resolve: 5.24.4 es-module-lexer: 2.3.1 eslint-scope: 5.1.1 events: 3.3.0 @@ -26856,7 +26814,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.3 - terser-webpack-plugin: 5.6.1(@swc/core@1.15.43(@swc/helpers@0.5.23))(esbuild@0.28.1)(lightningcss@1.33.0)(webpack@5.106.2(@swc/core@1.15.43(@swc/helpers@0.5.23))(esbuild@0.28.1)(lightningcss@1.33.0)) + terser-webpack-plugin: 5.6.1(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)(webpack@5.106.2(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) watchpack: 2.5.2 webpack-sources: 3.5.1 transitivePeerDependencies: @@ -26873,25 +26831,25 @@ snapshots: - postcss - uglify-js - webpack@5.108.4(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19): + webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3): dependencies: '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.17.0 - acorn-import-phases: 1.0.4(acorn@8.17.0) + acorn: 8.18.0 + acorn-import-phases: 1.0.4(acorn@8.18.0) browserslist: 4.28.6 chrome-trace-event: 1.0.4 - enhanced-resolve: 5.24.2 + enhanced-resolve: 5.24.4 es-module-lexer: 2.3.1 eslint-scope: 5.1.1 events: 3.3.0 graceful-fs: 4.2.11 loader-runner: 4.3.2 mime-db: 1.54.0 - minimizer-webpack-plugin: 5.6.1(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.19))(html-minifier-terser@7.2.0)(postcss@8.5.19)(webpack@5.108.4(postcss@8.5.19)) + minimizer-webpack-plugin: 5.6.1(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)) neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.3 @@ -26911,52 +26869,14 @@ snapshots: - postcss - uglify-js - webpack@5.108.4(postcss@8.5.19): - dependencies: - '@types/estree': 1.0.9 - '@types/json-schema': 7.0.15 - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/wasm-edit': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.17.0 - acorn-import-phases: 1.0.4(acorn@8.17.0) - browserslist: 4.28.6 - chrome-trace-event: 1.0.4 - enhanced-resolve: 5.24.2 - es-module-lexer: 2.3.1 - eslint-scope: 5.1.1 - events: 3.3.0 - graceful-fs: 4.2.11 - loader-runner: 4.3.2 - mime-db: 1.54.0 - minimizer-webpack-plugin: 5.6.1(postcss@8.5.19)(webpack@5.108.4(postcss@8.5.19)) - neo-async: 2.6.2 - schema-utils: 4.3.3 - tapable: 2.3.3 - watchpack: 2.5.2 - webpack-sources: 3.5.1 - transitivePeerDependencies: - - '@minify-html/node' - - '@swc/core' - - '@swc/css' - - '@swc/html' - - clean-css - - cssnano - - csso - - esbuild - - html-minifier-terser - - lightningcss - - postcss - - uglify-js - - webpackbar@7.0.0(webpack@5.108.4(postcss@8.5.19)): + webpackbar@7.0.0(webpack@5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3)): dependencies: ansis: 3.17.0 consola: 3.4.2 pretty-time: 1.1.0 std-env: 3.10.0 optionalDependencies: - webpack: 5.108.4(postcss@8.5.19) + webpack: 5.108.4(@swc/core@1.15.46(@swc/helpers@0.5.23))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.25))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.33.0)(postcss@8.5.25)(uglify-js@3.19.3) websocket-driver@0.7.5: dependencies: diff --git a/server/package.json b/server/package.json index 048ea06040..8e3559874f 100644 --- a/server/package.json +++ b/server/package.json @@ -49,14 +49,14 @@ "@nestjs/websockets": "^11.0.4", "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^2.0.0", - "@opentelemetry/exporter-prometheus": "^0.220.0", - "@opentelemetry/instrumentation-http": "^0.220.0", - "@opentelemetry/instrumentation-ioredis": "^0.68.0", - "@opentelemetry/instrumentation-nestjs-core": "^0.66.0", - "@opentelemetry/instrumentation-pg": "^0.72.0", + "@opentelemetry/exporter-prometheus": "^0.221.0", + "@opentelemetry/instrumentation-http": "^0.221.0", + "@opentelemetry/instrumentation-ioredis": "^0.69.0", + "@opentelemetry/instrumentation-nestjs-core": "^0.67.0", + "@opentelemetry/instrumentation-pg": "^0.73.0", "@opentelemetry/resources": "^2.0.1", "@opentelemetry/sdk-metrics": "^2.0.1", - "@opentelemetry/sdk-node": "^0.220.0", + "@opentelemetry/sdk-node": "^0.221.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@react-email/components": "^1.0.0", "@react-email/render": "^2.0.0", diff --git a/web/package.json b/web/package.json index b23bc94edb..aeaeb3ee20 100644 --- a/web/package.json +++ b/web/package.json @@ -105,7 +105,7 @@ "prettier-plugin-sort-json": "^4.1.1", "prettier-plugin-svelte": "^4.0.0", "rollup-plugin-visualizer": "^7.0.0", - "svelte": "5.56.5", + "svelte": "5.56.8", "svelte-check": "^4.4.6", "svelte-eslint-parser": "^1.3.3", "tailwindcss": "^4.2.4", diff --git a/web/src/lib/components/timeline/Month.svelte b/web/src/lib/components/timeline/Month.svelte index 7cbfd67ddf..423f0bec55 100644 --- a/web/src/lib/components/timeline/Month.svelte +++ b/web/src/lib/components/timeline/Month.svelte @@ -61,10 +61,8 @@ {@const isTimelineDaySelected = assetInteraction.selectedGroup.has(timelineDay.groupTitle)}
(choosePersonToMerge = false)}>
+
{#each potentialMergePeople as person (person.id)}
From 36cb86c886d7459e89e615283e748c4e6e6dc579 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:24:39 +0200 Subject: [PATCH 25/81] fix(deps): update dependency js-yaml to v5 [security] (#30440) Co-authored-by: Daniel Dietzler --- pnpm-lock.yaml | 4 ++-- server/package.json | 2 +- server/src/services/system-config.service.spec.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1f9120825d..1e9a9333c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -536,8 +536,8 @@ importers: specifier: ^6.0.0 version: 6.2.4 js-yaml: - specifier: ^4.1.0 - version: 4.3.0 + specifier: ^5.0.0 + version: 5.2.1 jsonwebtoken: specifier: ^9.0.2 version: 9.0.3 diff --git a/server/package.json b/server/package.json index 8e3559874f..7d5f73cc61 100644 --- a/server/package.json +++ b/server/package.json @@ -82,7 +82,7 @@ "i18n-iso-countries": "^7.6.0", "ioredis": "^5.8.2", "jose": "^6.0.0", - "js-yaml": "^4.1.0", + "js-yaml": "^5.0.0", "jsonwebtoken": "^9.0.2", "kysely": "0.28.17", "kysely-postgres-js": "^3.0.0", diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index d5404048c0..08851da96a 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -362,7 +362,7 @@ describe(SystemConfigService.name, () => { expect(mocks.logger.error).toHaveBeenCalledTimes(2); expect(mocks.logger.error.mock.calls[0][0]).toEqual('Unable to load configuration file: immich-config.json'); expect(mocks.logger.error.mock.calls[1][0].toString()).toEqual( - expect.stringContaining('YAMLException: duplicated mapping key (1:20)'), + expect.stringContaining('YAMLException: duplicated mapping key (1:21)'), ); }); From 7aa20683610e4c2a18c60d9ffe85f8c78f9cdc20 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:47:57 +0200 Subject: [PATCH 26/81] chore(deps): update dependency js-yaml to v5.2.2 [security] (#30441) --- pnpm-lock.yaml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e9a9333c5..b5503526c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -537,7 +537,7 @@ importers: version: 6.2.4 js-yaml: specifier: ^5.0.0 - version: 5.2.1 + version: 5.2.2 jsonwebtoken: specifier: ^9.0.2 version: 9.0.3 @@ -8796,6 +8796,10 @@ packages: resolution: {integrity: sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==} hasBin: true + js-yaml@5.2.2: + resolution: {integrity: sha512-dayzUzKkJ1MkuUtZglSebU43utNXH0OWQByK9rKOOuYIO8M5TV1y+n8ALMdG0rdzBnfNkOmZEqrURepb0ejqBw==} + hasBin: true + jsdom@26.1.0: resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} engines: {node: '>=18'} @@ -22082,6 +22086,10 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@5.2.2: + dependencies: + argparse: 2.0.1 + jsdom@26.1.0(canvas@3.2.3): dependencies: cssstyle: 4.6.0 From 627b52fdaf03d3757b3b7f3e68eb66d8725407b7 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Fri, 31 Jul 2026 05:31:48 -0700 Subject: [PATCH 27/81] chore(mobile): remove last committed generated files (#30426) --- .gitattributes | 6 - mobile/.gitignore | 1 + .../album/current_album.provider.g.dart | 26 - mobile/lib/routing/router.gr.dart | 1890 ----------------- 4 files changed, 1 insertion(+), 1922 deletions(-) delete mode 100644 mobile/lib/providers/album/current_album.provider.g.dart delete mode 100644 mobile/lib/routing/router.gr.dart diff --git a/.gitattributes b/.gitattributes index 14bba0a3eb..e96e0676ca 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,15 +1,9 @@ -mobile/lib/**/*.g.dart -diff -merge -mobile/lib/**/*.g.dart linguist-generated=true - mobile/android/**/*.g.kt -diff -merge mobile/android/**/*.g.kt linguist-generated=true mobile/ios/**/*.g.swift -diff -merge mobile/ios/**/*.g.swift linguist-generated=true -mobile/lib/**/*.drift.dart -diff -merge -mobile/lib/**/*.drift.dart linguist-generated=true - mobile/drift_schemas/main/drift_schema_*.json -diff -merge mobile/drift_schemas/main/drift_schema_*.json linguist-generated=true diff --git a/mobile/.gitignore b/mobile/.gitignore index bdddf7bbcc..6730e3c46a 100644 --- a/mobile/.gitignore +++ b/mobile/.gitignore @@ -32,6 +32,7 @@ .pub/ /build/ lib/**/*.drift.dart +lib/routing/router.gr.dart test/drift/main/generated/ # Pigeon related diff --git a/mobile/lib/providers/album/current_album.provider.g.dart b/mobile/lib/providers/album/current_album.provider.g.dart deleted file mode 100644 index b6d079231f..0000000000 --- a/mobile/lib/providers/album/current_album.provider.g.dart +++ /dev/null @@ -1,26 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'current_album.provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$currentAlbumHash() => r'61f00273d6b69da45add1532cc3d3a076ee55110'; - -/// See also [CurrentAlbum]. -@ProviderFor(CurrentAlbum) -final currentAlbumProvider = - AutoDisposeNotifierProvider.internal( - CurrentAlbum.new, - name: r'currentAlbumProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$currentAlbumHash, - dependencies: null, - allTransitiveDependencies: null, - ); - -typedef _$CurrentAlbum = AutoDisposeNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart deleted file mode 100644 index 1876a784dc..0000000000 --- a/mobile/lib/routing/router.gr.dart +++ /dev/null @@ -1,1890 +0,0 @@ -// dart format width=80 -// GENERATED CODE - DO NOT MODIFY BY HAND - -// ************************************************************************** -// AutoRouterGenerator -// ************************************************************************** - -// ignore_for_file: type=lint -// coverage:ignore-file - -part of 'router.dart'; - -/// generated route for -/// [AppLogDetailPage] -class AppLogDetailRoute extends PageRouteInfo { - AppLogDetailRoute({ - Key? key, - required LogMessage logMessage, - List? children, - }) : super( - AppLogDetailRoute.name, - args: AppLogDetailRouteArgs(key: key, logMessage: logMessage), - initialChildren: children, - ); - - static const String name = 'AppLogDetailRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AppLogDetailPage(key: args.key, logMessage: args.logMessage); - }, - ); -} - -class AppLogDetailRouteArgs { - const AppLogDetailRouteArgs({this.key, required this.logMessage}); - - final Key? key; - - final LogMessage logMessage; - - @override - String toString() { - return 'AppLogDetailRouteArgs{key: $key, logMessage: $logMessage}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! AppLogDetailRouteArgs) return false; - return key == other.key && logMessage == other.logMessage; - } - - @override - int get hashCode => key.hashCode ^ logMessage.hashCode; -} - -/// generated route for -/// [AppLogPage] -class AppLogRoute extends PageRouteInfo { - const AppLogRoute({List? children}) - : super(AppLogRoute.name, initialChildren: children); - - static const String name = 'AppLogRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const AppLogPage(); - }, - ); -} - -/// generated route for -/// [AssetTroubleshootPage] -class AssetTroubleshootRoute extends PageRouteInfo { - AssetTroubleshootRoute({ - Key? key, - required BaseAsset asset, - List? children, - }) : super( - AssetTroubleshootRoute.name, - args: AssetTroubleshootRouteArgs(key: key, asset: asset), - initialChildren: children, - ); - - static const String name = 'AssetTroubleshootRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AssetTroubleshootPage(key: args.key, asset: args.asset); - }, - ); -} - -class AssetTroubleshootRouteArgs { - const AssetTroubleshootRouteArgs({this.key, required this.asset}); - - final Key? key; - - final BaseAsset asset; - - @override - String toString() { - return 'AssetTroubleshootRouteArgs{key: $key, asset: $asset}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! AssetTroubleshootRouteArgs) return false; - return key == other.key && asset == other.asset; - } - - @override - int get hashCode => key.hashCode ^ asset.hashCode; -} - -/// generated route for -/// [AssetViewerPage] -class AssetViewerRoute extends PageRouteInfo { - AssetViewerRoute({ - Key? key, - required int initialIndex, - required TimelineService timelineService, - int? heroOffset, - RemoteAlbum? currentAlbum, - List? children, - }) : super( - AssetViewerRoute.name, - args: AssetViewerRouteArgs( - key: key, - initialIndex: initialIndex, - timelineService: timelineService, - heroOffset: heroOffset, - currentAlbum: currentAlbum, - ), - initialChildren: children, - ); - - static const String name = 'AssetViewerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return AssetViewerPage( - key: args.key, - initialIndex: args.initialIndex, - timelineService: args.timelineService, - heroOffset: args.heroOffset, - currentAlbum: args.currentAlbum, - ); - }, - ); -} - -class AssetViewerRouteArgs { - const AssetViewerRouteArgs({ - this.key, - required this.initialIndex, - required this.timelineService, - this.heroOffset, - this.currentAlbum, - }); - - final Key? key; - - final int initialIndex; - - final TimelineService timelineService; - - final int? heroOffset; - - final RemoteAlbum? currentAlbum; - - @override - String toString() { - return 'AssetViewerRouteArgs{key: $key, initialIndex: $initialIndex, timelineService: $timelineService, heroOffset: $heroOffset, currentAlbum: $currentAlbum}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! AssetViewerRouteArgs) return false; - return key == other.key && - initialIndex == other.initialIndex && - timelineService == other.timelineService && - heroOffset == other.heroOffset && - currentAlbum == other.currentAlbum; - } - - @override - int get hashCode => - key.hashCode ^ - initialIndex.hashCode ^ - timelineService.hashCode ^ - heroOffset.hashCode ^ - currentAlbum.hashCode; -} - -/// generated route for -/// [ChangePasswordPage] -class ChangePasswordRoute extends PageRouteInfo { - const ChangePasswordRoute({List? children}) - : super(ChangePasswordRoute.name, initialChildren: children); - - static const String name = 'ChangePasswordRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const ChangePasswordPage(); - }, - ); -} - -/// generated route for -/// [CleanupPreviewPage] -class CleanupPreviewRoute extends PageRouteInfo { - CleanupPreviewRoute({ - Key? key, - required List assets, - List? children, - }) : super( - CleanupPreviewRoute.name, - args: CleanupPreviewRouteArgs(key: key, assets: assets), - initialChildren: children, - ); - - static const String name = 'CleanupPreviewRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return CleanupPreviewPage(key: args.key, assets: args.assets); - }, - ); -} - -class CleanupPreviewRouteArgs { - const CleanupPreviewRouteArgs({this.key, required this.assets}); - - final Key? key; - - final List assets; - - @override - String toString() { - return 'CleanupPreviewRouteArgs{key: $key, assets: $assets}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! CleanupPreviewRouteArgs) return false; - return key == other.key && - const ListEquality().equals(assets, other.assets); - } - - @override - int get hashCode => - key.hashCode ^ const ListEquality().hash(assets); -} - -/// generated route for -/// [DownloadInfoPage] -class DownloadInfoRoute extends PageRouteInfo { - const DownloadInfoRoute({List? children}) - : super(DownloadInfoRoute.name, initialChildren: children); - - static const String name = 'DownloadInfoRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DownloadInfoPage(); - }, - ); -} - -/// generated route for -/// [DriftActivitiesPage] -class DriftActivitiesRoute extends PageRouteInfo { - DriftActivitiesRoute({ - Key? key, - required RemoteAlbum album, - String? assetId, - String? assetName, - List? children, - }) : super( - DriftActivitiesRoute.name, - args: DriftActivitiesRouteArgs( - key: key, - album: album, - assetId: assetId, - assetName: assetName, - ), - initialChildren: children, - ); - - static const String name = 'DriftActivitiesRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftActivitiesPage( - key: args.key, - album: args.album, - assetId: args.assetId, - assetName: args.assetName, - ); - }, - ); -} - -class DriftActivitiesRouteArgs { - const DriftActivitiesRouteArgs({ - this.key, - required this.album, - this.assetId, - this.assetName, - }); - - final Key? key; - - final RemoteAlbum album; - - final String? assetId; - - final String? assetName; - - @override - String toString() { - return 'DriftActivitiesRouteArgs{key: $key, album: $album, assetId: $assetId, assetName: $assetName}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! DriftActivitiesRouteArgs) return false; - return key == other.key && - album == other.album && - assetId == other.assetId && - assetName == other.assetName; - } - - @override - int get hashCode => - key.hashCode ^ album.hashCode ^ assetId.hashCode ^ assetName.hashCode; -} - -/// generated route for -/// [DriftAlbumOptionsPage] -class DriftAlbumOptionsRoute extends PageRouteInfo { - DriftAlbumOptionsRoute({ - Key? key, - required RemoteAlbum album, - List? children, - }) : super( - DriftAlbumOptionsRoute.name, - args: DriftAlbumOptionsRouteArgs(key: key, album: album), - initialChildren: children, - ); - - static const String name = 'DriftAlbumOptionsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftAlbumOptionsPage(key: args.key, album: args.album); - }, - ); -} - -class DriftAlbumOptionsRouteArgs { - const DriftAlbumOptionsRouteArgs({this.key, required this.album}); - - final Key? key; - - final RemoteAlbum album; - - @override - String toString() { - return 'DriftAlbumOptionsRouteArgs{key: $key, album: $album}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! DriftAlbumOptionsRouteArgs) return false; - return key == other.key && album == other.album; - } - - @override - int get hashCode => key.hashCode ^ album.hashCode; -} - -/// generated route for -/// [DriftAlbumsPage] -class DriftAlbumsRoute extends PageRouteInfo { - const DriftAlbumsRoute({List? children}) - : super(DriftAlbumsRoute.name, initialChildren: children); - - static const String name = 'DriftAlbumsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftAlbumsPage(); - }, - ); -} - -/// generated route for -/// [DriftArchivePage] -class DriftArchiveRoute extends PageRouteInfo { - const DriftArchiveRoute({List? children}) - : super(DriftArchiveRoute.name, initialChildren: children); - - static const String name = 'DriftArchiveRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftArchivePage(); - }, - ); -} - -/// generated route for -/// [DriftAssetSelectionTimelinePage] -class DriftAssetSelectionTimelineRoute - extends PageRouteInfo { - DriftAssetSelectionTimelineRoute({ - Key? key, - Set lockedSelectionAssets = const {}, - List? children, - }) : super( - DriftAssetSelectionTimelineRoute.name, - args: DriftAssetSelectionTimelineRouteArgs( - key: key, - lockedSelectionAssets: lockedSelectionAssets, - ), - initialChildren: children, - ); - - static const String name = 'DriftAssetSelectionTimelineRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const DriftAssetSelectionTimelineRouteArgs(), - ); - return DriftAssetSelectionTimelinePage( - key: args.key, - lockedSelectionAssets: args.lockedSelectionAssets, - ); - }, - ); -} - -class DriftAssetSelectionTimelineRouteArgs { - const DriftAssetSelectionTimelineRouteArgs({ - this.key, - this.lockedSelectionAssets = const {}, - }); - - final Key? key; - - final Set lockedSelectionAssets; - - @override - String toString() { - return 'DriftAssetSelectionTimelineRouteArgs{key: $key, lockedSelectionAssets: $lockedSelectionAssets}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! DriftAssetSelectionTimelineRouteArgs) return false; - return key == other.key && - const SetEquality().equals( - lockedSelectionAssets, - other.lockedSelectionAssets, - ); - } - - @override - int get hashCode => - key.hashCode ^ const SetEquality().hash(lockedSelectionAssets); -} - -/// generated route for -/// [DriftBackupAlbumSelectionPage] -class DriftBackupAlbumSelectionRoute extends PageRouteInfo { - const DriftBackupAlbumSelectionRoute({List? children}) - : super(DriftBackupAlbumSelectionRoute.name, initialChildren: children); - - static const String name = 'DriftBackupAlbumSelectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftBackupAlbumSelectionPage(); - }, - ); -} - -/// generated route for -/// [DriftBackupAssetDetailPage] -class DriftBackupAssetDetailRoute extends PageRouteInfo { - const DriftBackupAssetDetailRoute({List? children}) - : super(DriftBackupAssetDetailRoute.name, initialChildren: children); - - static const String name = 'DriftBackupAssetDetailRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftBackupAssetDetailPage(); - }, - ); -} - -/// generated route for -/// [DriftBackupOptionsPage] -class DriftBackupOptionsRoute extends PageRouteInfo { - const DriftBackupOptionsRoute({List? children}) - : super(DriftBackupOptionsRoute.name, initialChildren: children); - - static const String name = 'DriftBackupOptionsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftBackupOptionsPage(); - }, - ); -} - -/// generated route for -/// [DriftBackupPage] -class DriftBackupRoute extends PageRouteInfo { - const DriftBackupRoute({List? children}) - : super(DriftBackupRoute.name, initialChildren: children); - - static const String name = 'DriftBackupRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftBackupPage(); - }, - ); -} - -/// generated route for -/// [DriftCreateAlbumPage] -class DriftCreateAlbumRoute extends PageRouteInfo { - const DriftCreateAlbumRoute({List? children}) - : super(DriftCreateAlbumRoute.name, initialChildren: children); - - static const String name = 'DriftCreateAlbumRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftCreateAlbumPage(); - }, - ); -} - -/// generated route for -/// [DriftEditImagePage] -class DriftEditImageRoute extends PageRouteInfo { - DriftEditImageRoute({ - Key? key, - required Image image, - required Future Function(List) applyEdits, - List? children, - }) : super( - DriftEditImageRoute.name, - args: DriftEditImageRouteArgs( - key: key, - image: image, - applyEdits: applyEdits, - ), - initialChildren: children, - ); - - static const String name = 'DriftEditImageRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftEditImagePage( - key: args.key, - image: args.image, - applyEdits: args.applyEdits, - ); - }, - ); -} - -class DriftEditImageRouteArgs { - const DriftEditImageRouteArgs({ - this.key, - required this.image, - required this.applyEdits, - }); - - final Key? key; - - final Image image; - - final Future Function(List) applyEdits; - - @override - String toString() { - return 'DriftEditImageRouteArgs{key: $key, image: $image, applyEdits: $applyEdits}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! DriftEditImageRouteArgs) return false; - return key == other.key && image == other.image; - } - - @override - int get hashCode => key.hashCode ^ image.hashCode; -} - -/// generated route for -/// [DriftFavoritePage] -class DriftFavoriteRoute extends PageRouteInfo { - const DriftFavoriteRoute({List? children}) - : super(DriftFavoriteRoute.name, initialChildren: children); - - static const String name = 'DriftFavoriteRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftFavoritePage(); - }, - ); -} - -/// generated route for -/// [DriftLibraryPage] -class DriftLibraryRoute extends PageRouteInfo { - const DriftLibraryRoute({List? children}) - : super(DriftLibraryRoute.name, initialChildren: children); - - static const String name = 'DriftLibraryRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftLibraryPage(); - }, - ); -} - -/// generated route for -/// [DriftLocalAlbumsPage] -class DriftLocalAlbumsRoute extends PageRouteInfo { - const DriftLocalAlbumsRoute({List? children}) - : super(DriftLocalAlbumsRoute.name, initialChildren: children); - - static const String name = 'DriftLocalAlbumsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftLocalAlbumsPage(); - }, - ); -} - -/// generated route for -/// [DriftLockedFolderPage] -class DriftLockedFolderRoute extends PageRouteInfo { - const DriftLockedFolderRoute({List? children}) - : super(DriftLockedFolderRoute.name, initialChildren: children); - - static const String name = 'DriftLockedFolderRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftLockedFolderPage(); - }, - ); -} - -/// generated route for -/// [DriftMapPage] -class DriftMapRoute extends PageRouteInfo { - DriftMapRoute({ - Key? key, - LatLng? initialLocation, - List? children, - }) : super( - DriftMapRoute.name, - args: DriftMapRouteArgs(key: key, initialLocation: initialLocation), - initialChildren: children, - ); - - static const String name = 'DriftMapRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const DriftMapRouteArgs(), - ); - return DriftMapPage(key: args.key, initialLocation: args.initialLocation); - }, - ); -} - -class DriftMapRouteArgs { - const DriftMapRouteArgs({this.key, this.initialLocation}); - - final Key? key; - - final LatLng? initialLocation; - - @override - String toString() { - return 'DriftMapRouteArgs{key: $key, initialLocation: $initialLocation}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! DriftMapRouteArgs) return false; - return key == other.key && initialLocation == other.initialLocation; - } - - @override - int get hashCode => key.hashCode ^ initialLocation.hashCode; -} - -/// generated route for -/// [DriftMemoryPage] -class DriftMemoryRoute extends PageRouteInfo { - DriftMemoryRoute({ - required List memories, - required int memoryIndex, - Key? key, - List? children, - }) : super( - DriftMemoryRoute.name, - args: DriftMemoryRouteArgs( - memories: memories, - memoryIndex: memoryIndex, - key: key, - ), - initialChildren: children, - ); - - static const String name = 'DriftMemoryRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftMemoryPage( - memories: args.memories, - memoryIndex: args.memoryIndex, - key: args.key, - ); - }, - ); -} - -class DriftMemoryRouteArgs { - const DriftMemoryRouteArgs({ - required this.memories, - required this.memoryIndex, - this.key, - }); - - final List memories; - - final int memoryIndex; - - final Key? key; - - @override - String toString() { - return 'DriftMemoryRouteArgs{memories: $memories, memoryIndex: $memoryIndex, key: $key}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! DriftMemoryRouteArgs) return false; - return const ListEquality().equals(memories, other.memories) && - memoryIndex == other.memoryIndex && - key == other.key; - } - - @override - int get hashCode => - const ListEquality().hash(memories) ^ - memoryIndex.hashCode ^ - key.hashCode; -} - -/// generated route for -/// [DriftPartnerDetailPage] -class DriftPartnerDetailRoute - extends PageRouteInfo { - DriftPartnerDetailRoute({ - Key? key, - required Partner partner, - List? children, - }) : super( - DriftPartnerDetailRoute.name, - args: DriftPartnerDetailRouteArgs(key: key, partner: partner), - initialChildren: children, - ); - - static const String name = 'DriftPartnerDetailRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftPartnerDetailPage(key: args.key, partner: args.partner); - }, - ); -} - -class DriftPartnerDetailRouteArgs { - const DriftPartnerDetailRouteArgs({this.key, required this.partner}); - - final Key? key; - - final Partner partner; - - @override - String toString() { - return 'DriftPartnerDetailRouteArgs{key: $key, partner: $partner}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! DriftPartnerDetailRouteArgs) return false; - return key == other.key && partner == other.partner; - } - - @override - int get hashCode => key.hashCode ^ partner.hashCode; -} - -/// generated route for -/// [DriftPeopleCollectionPage] -class DriftPeopleCollectionRoute extends PageRouteInfo { - const DriftPeopleCollectionRoute({List? children}) - : super(DriftPeopleCollectionRoute.name, initialChildren: children); - - static const String name = 'DriftPeopleCollectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftPeopleCollectionPage(); - }, - ); -} - -/// generated route for -/// [DriftPersonPage] -class DriftPersonRoute extends PageRouteInfo { - DriftPersonRoute({ - Key? key, - required DriftPerson person, - List? children, - }) : super( - DriftPersonRoute.name, - args: DriftPersonRouteArgs(key: key, person: person), - initialChildren: children, - ); - - static const String name = 'DriftPersonRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftPersonPage(key: args.key, person: args.person); - }, - ); -} - -class DriftPersonRouteArgs { - const DriftPersonRouteArgs({this.key, required this.person}); - - final Key? key; - - final DriftPerson person; - - @override - String toString() { - return 'DriftPersonRouteArgs{key: $key, person: $person}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! DriftPersonRouteArgs) return false; - return key == other.key && person == other.person; - } - - @override - int get hashCode => key.hashCode ^ person.hashCode; -} - -/// generated route for -/// [DriftPlaceDetailPage] -class DriftPlaceDetailRoute extends PageRouteInfo { - DriftPlaceDetailRoute({ - Key? key, - required String place, - List? children, - }) : super( - DriftPlaceDetailRoute.name, - args: DriftPlaceDetailRouteArgs(key: key, place: place), - initialChildren: children, - ); - - static const String name = 'DriftPlaceDetailRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftPlaceDetailPage(key: args.key, place: args.place); - }, - ); -} - -class DriftPlaceDetailRouteArgs { - const DriftPlaceDetailRouteArgs({this.key, required this.place}); - - final Key? key; - - final String place; - - @override - String toString() { - return 'DriftPlaceDetailRouteArgs{key: $key, place: $place}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! DriftPlaceDetailRouteArgs) return false; - return key == other.key && place == other.place; - } - - @override - int get hashCode => key.hashCode ^ place.hashCode; -} - -/// generated route for -/// [DriftPlacePage] -class DriftPlaceRoute extends PageRouteInfo { - DriftPlaceRoute({ - Key? key, - LatLng? currentLocation, - List? children, - }) : super( - DriftPlaceRoute.name, - args: DriftPlaceRouteArgs(key: key, currentLocation: currentLocation), - initialChildren: children, - ); - - static const String name = 'DriftPlaceRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const DriftPlaceRouteArgs(), - ); - return DriftPlacePage( - key: args.key, - currentLocation: args.currentLocation, - ); - }, - ); -} - -class DriftPlaceRouteArgs { - const DriftPlaceRouteArgs({this.key, this.currentLocation}); - - final Key? key; - - final LatLng? currentLocation; - - @override - String toString() { - return 'DriftPlaceRouteArgs{key: $key, currentLocation: $currentLocation}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! DriftPlaceRouteArgs) return false; - return key == other.key && currentLocation == other.currentLocation; - } - - @override - int get hashCode => key.hashCode ^ currentLocation.hashCode; -} - -/// generated route for -/// [DriftRecentlyAddedPage] -class DriftRecentlyAddedRoute extends PageRouteInfo { - const DriftRecentlyAddedRoute({List? children}) - : super(DriftRecentlyAddedRoute.name, initialChildren: children); - - static const String name = 'DriftRecentlyAddedRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftRecentlyAddedPage(); - }, - ); -} - -/// generated route for -/// [DriftRecentlyTakenPage] -class DriftRecentlyTakenRoute extends PageRouteInfo { - const DriftRecentlyTakenRoute({List? children}) - : super(DriftRecentlyTakenRoute.name, initialChildren: children); - - static const String name = 'DriftRecentlyTakenRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftRecentlyTakenPage(); - }, - ); -} - -/// generated route for -/// [DriftSearchPage] -class DriftSearchRoute extends PageRouteInfo { - const DriftSearchRoute({List? children}) - : super(DriftSearchRoute.name, initialChildren: children); - - static const String name = 'DriftSearchRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftSearchPage(); - }, - ); -} - -/// generated route for -/// [DriftSlideshowPage] -class DriftSlideshowRoute extends PageRouteInfo { - DriftSlideshowRoute({ - Key? key, - required TimelineService timeline, - List? children, - }) : super( - DriftSlideshowRoute.name, - args: DriftSlideshowRouteArgs(key: key, timeline: timeline), - initialChildren: children, - ); - - static const String name = 'DriftSlideshowRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftSlideshowPage(key: args.key, timeline: args.timeline); - }, - ); -} - -class DriftSlideshowRouteArgs { - const DriftSlideshowRouteArgs({this.key, required this.timeline}); - - final Key? key; - - final TimelineService timeline; - - @override - String toString() { - return 'DriftSlideshowRouteArgs{key: $key, timeline: $timeline}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! DriftSlideshowRouteArgs) return false; - return key == other.key && timeline == other.timeline; - } - - @override - int get hashCode => key.hashCode ^ timeline.hashCode; -} - -/// generated route for -/// [DriftTrashPage] -class DriftTrashRoute extends PageRouteInfo { - const DriftTrashRoute({List? children}) - : super(DriftTrashRoute.name, initialChildren: children); - - static const String name = 'DriftTrashRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftTrashPage(); - }, - ); -} - -/// generated route for -/// [DriftUploadDetailPage] -class DriftUploadDetailRoute extends PageRouteInfo { - const DriftUploadDetailRoute({List? children}) - : super(DriftUploadDetailRoute.name, initialChildren: children); - - static const String name = 'DriftUploadDetailRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftUploadDetailPage(); - }, - ); -} - -/// generated route for -/// [DriftUserSelectionPage] -class DriftUserSelectionRoute - extends PageRouteInfo { - DriftUserSelectionRoute({ - Key? key, - required RemoteAlbum album, - List? children, - }) : super( - DriftUserSelectionRoute.name, - args: DriftUserSelectionRouteArgs(key: key, album: album), - initialChildren: children, - ); - - static const String name = 'DriftUserSelectionRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return DriftUserSelectionPage(key: args.key, album: args.album); - }, - ); -} - -class DriftUserSelectionRouteArgs { - const DriftUserSelectionRouteArgs({this.key, required this.album}); - - final Key? key; - - final RemoteAlbum album; - - @override - String toString() { - return 'DriftUserSelectionRouteArgs{key: $key, album: $album}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! DriftUserSelectionRouteArgs) return false; - return key == other.key && album == other.album; - } - - @override - int get hashCode => key.hashCode ^ album.hashCode; -} - -/// generated route for -/// [DriftVideoPage] -class DriftVideoRoute extends PageRouteInfo { - const DriftVideoRoute({List? children}) - : super(DriftVideoRoute.name, initialChildren: children); - - static const String name = 'DriftVideoRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const DriftVideoPage(); - }, - ); -} - -/// generated route for -/// [FolderPage] -class FolderRoute extends PageRouteInfo { - FolderRoute({ - Key? key, - RecursiveFolder? folder, - List? children, - }) : super( - FolderRoute.name, - args: FolderRouteArgs(key: key, folder: folder), - initialChildren: children, - ); - - static const String name = 'FolderRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const FolderRouteArgs(), - ); - return FolderPage(key: args.key, folder: args.folder); - }, - ); -} - -class FolderRouteArgs { - const FolderRouteArgs({this.key, this.folder}); - - final Key? key; - - final RecursiveFolder? folder; - - @override - String toString() { - return 'FolderRouteArgs{key: $key, folder: $folder}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! FolderRouteArgs) return false; - return key == other.key && folder == other.folder; - } - - @override - int get hashCode => key.hashCode ^ folder.hashCode; -} - -/// generated route for -/// [HeaderSettingsPage] -class HeaderSettingsRoute extends PageRouteInfo { - const HeaderSettingsRoute({List? children}) - : super(HeaderSettingsRoute.name, initialChildren: children); - - static const String name = 'HeaderSettingsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const HeaderSettingsPage(); - }, - ); -} - -/// generated route for -/// [LocalMediaSummaryPage] -class LocalMediaSummaryRoute extends PageRouteInfo { - const LocalMediaSummaryRoute({List? children}) - : super(LocalMediaSummaryRoute.name, initialChildren: children); - - static const String name = 'LocalMediaSummaryRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const LocalMediaSummaryPage(); - }, - ); -} - -/// generated route for -/// [LocalTimelinePage] -class LocalTimelineRoute extends PageRouteInfo { - LocalTimelineRoute({ - Key? key, - required LocalAlbum album, - List? children, - }) : super( - LocalTimelineRoute.name, - args: LocalTimelineRouteArgs(key: key, album: album), - initialChildren: children, - ); - - static const String name = 'LocalTimelineRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return LocalTimelinePage(key: args.key, album: args.album); - }, - ); -} - -class LocalTimelineRouteArgs { - const LocalTimelineRouteArgs({this.key, required this.album}); - - final Key? key; - - final LocalAlbum album; - - @override - String toString() { - return 'LocalTimelineRouteArgs{key: $key, album: $album}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! LocalTimelineRouteArgs) return false; - return key == other.key && album == other.album; - } - - @override - int get hashCode => key.hashCode ^ album.hashCode; -} - -/// generated route for -/// [LoginPage] -class LoginRoute extends PageRouteInfo { - const LoginRoute({List? children}) - : super(LoginRoute.name, initialChildren: children); - - static const String name = 'LoginRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const LoginPage(); - }, - ); -} - -/// generated route for -/// [MainTimelinePage] -class MainTimelineRoute extends PageRouteInfo { - const MainTimelineRoute({List? children}) - : super(MainTimelineRoute.name, initialChildren: children); - - static const String name = 'MainTimelineRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const MainTimelinePage(); - }, - ); -} - -/// generated route for -/// [MapLocationPickerPage] -class MapLocationPickerRoute extends PageRouteInfo { - MapLocationPickerRoute({ - Key? key, - LatLng initialLatLng = const LatLng(0, 0), - List? children, - }) : super( - MapLocationPickerRoute.name, - args: MapLocationPickerRouteArgs( - key: key, - initialLatLng: initialLatLng, - ), - initialChildren: children, - ); - - static const String name = 'MapLocationPickerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const MapLocationPickerRouteArgs(), - ); - return MapLocationPickerPage( - key: args.key, - initialLatLng: args.initialLatLng, - ); - }, - ); -} - -class MapLocationPickerRouteArgs { - const MapLocationPickerRouteArgs({ - this.key, - this.initialLatLng = const LatLng(0, 0), - }); - - final Key? key; - - final LatLng initialLatLng; - - @override - String toString() { - return 'MapLocationPickerRouteArgs{key: $key, initialLatLng: $initialLatLng}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! MapLocationPickerRouteArgs) return false; - return key == other.key && initialLatLng == other.initialLatLng; - } - - @override - int get hashCode => key.hashCode ^ initialLatLng.hashCode; -} - -/// generated route for -/// [PartnerPage] -class PartnerRoute extends PageRouteInfo { - const PartnerRoute({List? children}) - : super(PartnerRoute.name, initialChildren: children); - - static const String name = 'PartnerRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const PartnerPage(); - }, - ); -} - -/// generated route for -/// [PinAuthPage] -class PinAuthRoute extends PageRouteInfo { - PinAuthRoute({ - Key? key, - bool createPinCode = false, - List? children, - }) : super( - PinAuthRoute.name, - args: PinAuthRouteArgs(key: key, createPinCode: createPinCode), - initialChildren: children, - ); - - static const String name = 'PinAuthRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const PinAuthRouteArgs(), - ); - return PinAuthPage(key: args.key, createPinCode: args.createPinCode); - }, - ); -} - -class PinAuthRouteArgs { - const PinAuthRouteArgs({this.key, this.createPinCode = false}); - - final Key? key; - - final bool createPinCode; - - @override - String toString() { - return 'PinAuthRouteArgs{key: $key, createPinCode: $createPinCode}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! PinAuthRouteArgs) return false; - return key == other.key && createPinCode == other.createPinCode; - } - - @override - int get hashCode => key.hashCode ^ createPinCode.hashCode; -} - -/// generated route for -/// [ProfilePictureCropPage] -class ProfilePictureCropRoute - extends PageRouteInfo { - ProfilePictureCropRoute({ - Key? key, - required BaseAsset asset, - List? children, - }) : super( - ProfilePictureCropRoute.name, - args: ProfilePictureCropRouteArgs(key: key, asset: asset), - initialChildren: children, - ); - - static const String name = 'ProfilePictureCropRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return ProfilePictureCropPage(key: args.key, asset: args.asset); - }, - ); -} - -class ProfilePictureCropRouteArgs { - const ProfilePictureCropRouteArgs({this.key, required this.asset}); - - final Key? key; - - final BaseAsset asset; - - @override - String toString() { - return 'ProfilePictureCropRouteArgs{key: $key, asset: $asset}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! ProfilePictureCropRouteArgs) return false; - return key == other.key && asset == other.asset; - } - - @override - int get hashCode => key.hashCode ^ asset.hashCode; -} - -/// generated route for -/// [RemoteAlbumPage] -class RemoteAlbumRoute extends PageRouteInfo { - RemoteAlbumRoute({ - Key? key, - required RemoteAlbum album, - List? children, - }) : super( - RemoteAlbumRoute.name, - args: RemoteAlbumRouteArgs(key: key, album: album), - initialChildren: children, - ); - - static const String name = 'RemoteAlbumRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return RemoteAlbumPage(key: args.key, album: args.album); - }, - ); -} - -class RemoteAlbumRouteArgs { - const RemoteAlbumRouteArgs({this.key, required this.album}); - - final Key? key; - - final RemoteAlbum album; - - @override - String toString() { - return 'RemoteAlbumRouteArgs{key: $key, album: $album}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! RemoteAlbumRouteArgs) return false; - return key == other.key && album == other.album; - } - - @override - int get hashCode => key.hashCode ^ album.hashCode; -} - -/// generated route for -/// [RemoteMediaSummaryPage] -class RemoteMediaSummaryRoute extends PageRouteInfo { - const RemoteMediaSummaryRoute({List? children}) - : super(RemoteMediaSummaryRoute.name, initialChildren: children); - - static const String name = 'RemoteMediaSummaryRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const RemoteMediaSummaryPage(); - }, - ); -} - -/// generated route for -/// [SettingsPage] -class SettingsRoute extends PageRouteInfo { - const SettingsRoute({List? children}) - : super(SettingsRoute.name, initialChildren: children); - - static const String name = 'SettingsRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const SettingsPage(); - }, - ); -} - -/// generated route for -/// [SettingsSubPage] -class SettingsSubRoute extends PageRouteInfo { - SettingsSubRoute({ - required SettingSection section, - Key? key, - List? children, - }) : super( - SettingsSubRoute.name, - args: SettingsSubRouteArgs(section: section, key: key), - initialChildren: children, - ); - - static const String name = 'SettingsSubRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return SettingsSubPage(args.section, key: args.key); - }, - ); -} - -class SettingsSubRouteArgs { - const SettingsSubRouteArgs({required this.section, this.key}); - - final SettingSection section; - - final Key? key; - - @override - String toString() { - return 'SettingsSubRouteArgs{section: $section, key: $key}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! SettingsSubRouteArgs) return false; - return section == other.section && key == other.key; - } - - @override - int get hashCode => section.hashCode ^ key.hashCode; -} - -/// generated route for -/// [ShareIntentPage] -class ShareIntentRoute extends PageRouteInfo { - ShareIntentRoute({ - Key? key, - required List attachments, - List? children, - }) : super( - ShareIntentRoute.name, - args: ShareIntentRouteArgs(key: key, attachments: attachments), - initialChildren: children, - ); - - static const String name = 'ShareIntentRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs(); - return ShareIntentPage(key: args.key, attachments: args.attachments); - }, - ); -} - -class ShareIntentRouteArgs { - const ShareIntentRouteArgs({this.key, required this.attachments}); - - final Key? key; - - final List attachments; - - @override - String toString() { - return 'ShareIntentRouteArgs{key: $key, attachments: $attachments}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! ShareIntentRouteArgs) return false; - return key == other.key && - const ListEquality().equals( - attachments, - other.attachments, - ); - } - - @override - int get hashCode => - key.hashCode ^ - const ListEquality().hash(attachments); -} - -/// generated route for -/// [SharedLinkEditPage] -class SharedLinkEditRoute extends PageRouteInfo { - SharedLinkEditRoute({ - Key? key, - SharedLink? existingLink, - List? assetsList, - String? albumId, - List? children, - }) : super( - SharedLinkEditRoute.name, - args: SharedLinkEditRouteArgs( - key: key, - existingLink: existingLink, - assetsList: assetsList, - albumId: albumId, - ), - initialChildren: children, - ); - - static const String name = 'SharedLinkEditRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - final args = data.argsAs( - orElse: () => const SharedLinkEditRouteArgs(), - ); - return SharedLinkEditPage( - key: args.key, - existingLink: args.existingLink, - assetsList: args.assetsList, - albumId: args.albumId, - ); - }, - ); -} - -class SharedLinkEditRouteArgs { - const SharedLinkEditRouteArgs({ - this.key, - this.existingLink, - this.assetsList, - this.albumId, - }); - - final Key? key; - - final SharedLink? existingLink; - - final List? assetsList; - - final String? albumId; - - @override - String toString() { - return 'SharedLinkEditRouteArgs{key: $key, existingLink: $existingLink, assetsList: $assetsList, albumId: $albumId}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! SharedLinkEditRouteArgs) return false; - return key == other.key && - existingLink == other.existingLink && - const ListEquality().equals(assetsList, other.assetsList) && - albumId == other.albumId; - } - - @override - int get hashCode => - key.hashCode ^ - existingLink.hashCode ^ - const ListEquality().hash(assetsList) ^ - albumId.hashCode; -} - -/// generated route for -/// [SharedLinkPage] -class SharedLinkRoute extends PageRouteInfo { - const SharedLinkRoute({List? children}) - : super(SharedLinkRoute.name, initialChildren: children); - - static const String name = 'SharedLinkRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const SharedLinkPage(); - }, - ); -} - -/// generated route for -/// [SplashScreenPage] -class SplashScreenRoute extends PageRouteInfo { - const SplashScreenRoute({List? children}) - : super(SplashScreenRoute.name, initialChildren: children); - - static const String name = 'SplashScreenRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const SplashScreenPage(); - }, - ); -} - -/// generated route for -/// [SyncStatusPage] -class SyncStatusRoute extends PageRouteInfo { - const SyncStatusRoute({List? children}) - : super(SyncStatusRoute.name, initialChildren: children); - - static const String name = 'SyncStatusRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const SyncStatusPage(); - }, - ); -} - -/// generated route for -/// [TabShellPage] -class TabShellRoute extends PageRouteInfo { - const TabShellRoute({List? children}) - : super(TabShellRoute.name, initialChildren: children); - - static const String name = 'TabShellRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const TabShellPage(); - }, - ); -} - -/// generated route for -/// [WhatsNewPage] -class WhatsNewRoute extends PageRouteInfo { - const WhatsNewRoute({List? children}) - : super(WhatsNewRoute.name, initialChildren: children); - - static const String name = 'WhatsNewRoute'; - - static PageInfo page = PageInfo( - name, - builder: (data) { - return const WhatsNewPage(); - }, - ); -} From 920552c1c245411e70c0179ba3577af2d4d74320 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Fri, 31 Jul 2026 05:52:59 -0700 Subject: [PATCH 28/81] chore(mobile): depend on OpenAPI generation in appropriate codegen tasks (#30425) --- mise.toml | 2 +- mobile/mise.toml | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/mise.toml b/mise.toml index f01c07f980..ad7fdf5a90 100644 --- a/mise.toml +++ b/mise.toml @@ -69,7 +69,7 @@ sources = [ "patch/*", "bin/generate-dart-sdk.sh", ] -outputs = ["../mobile/openapi/lib/"] +outputs = ["../mobile/generated/openapi/"] run = "bash ./bin/generate-dart-sdk.sh" [tasks.open-api] diff --git a/mobile/mise.toml b/mobile/mise.toml index ce7c626e18..91cdb77a19 100644 --- a/mobile/mise.toml +++ b/mobile/mise.toml @@ -17,7 +17,7 @@ windows-x64 = { asset_pattern = "dcm-windows-release.zip" } [tasks.codegen] alias = "codegen" description = "Generate all codegen artifacts" -depends = ["codegen:dart", "codegen:drift:schema", "codegen:pigeon", "codegen:translation"] +depends = ["//:open-api-dart", "codegen:dart", "codegen:drift:schema", "codegen:pigeon", "codegen:translation"] [tasks."codegen:dart"] description = "Execute build_runner to auto-generate dart code" @@ -32,12 +32,14 @@ run = [ "dart run build_runner build --delete-conflicting-outputs", "dart format lib/routing/router.gr.dart", ] +depends = ["//:open-api-dart"] [tasks."codegen:drift:schema"] description = "Generate Drift migration schema test code" sources = ["drift_schemas/main/*.json"] outputs = { auto = true } run = "dart run drift_dev schema generate --data-classes --companions drift_schemas/main/ test/drift/main/generated/" +depends = ["//:open-api-dart"] [tasks."codegen:watch"] alias = "watch" @@ -51,6 +53,7 @@ run = [ "ls pigeon/*.dart | xargs -n1 -P4 -I{} dart run pigeon --input {}", "dart format lib/platform/", ] +depends = ["//:open-api-dart"] [tasks."codegen:translation"] alias = "translation" @@ -123,6 +126,7 @@ run = [ "dart run easy_localization:generate -S ../i18n", "dart format lib/generated/codegen_loader.g.dart", ] +depends = ["//:open-api-dart"] [tasks."i18n:keys"] description = "Generate i18n keys" From dd8d068d4438ae24942201459391c722b002f3ee Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Fri, 31 Jul 2026 18:44:33 +0530 Subject: [PATCH 29/81] chore: drop removed build_runner flag (#30427) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- mobile/mise.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mobile/mise.toml b/mobile/mise.toml index 91cdb77a19..ee312178a4 100644 --- a/mobile/mise.toml +++ b/mobile/mise.toml @@ -29,7 +29,7 @@ sources = [ ] outputs = { auto = true } run = [ - "dart run build_runner build --delete-conflicting-outputs", + "dart run build_runner build", "dart format lib/routing/router.gr.dart", ] depends = ["//:open-api-dart"] @@ -44,7 +44,7 @@ depends = ["//:open-api-dart"] [tasks."codegen:watch"] alias = "watch" description = "Watch and auto-generate dart code" -run = "dart run build_runner watch --delete-conflicting-outputs" +run = "dart run build_runner watch" [tasks."codegen:pigeon"] alias = "pigeon" From 483b375c26c91e9ad3d42666fff58b3fbda1164a Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Fri, 31 Jul 2026 07:37:12 -0700 Subject: [PATCH 30/81] fix(mobile): resolve owned assets when partner owns identical asset (#30137) * fix(mobile): resolve owned assets when partner owns identical asset * Add limit to auth user expressions * Change debug method semantics --- mobile/lib/domain/services/asset.service.dart | 4 +- .../repositories/local_asset.repository.dart | 22 ++-- .../repositories/remote_asset.repository.dart | 4 +- .../repositories/timeline.repository.dart | 12 +-- .../trashed_local_asset.repository.dart | 9 +- .../pages/drift_asset_troubleshoot.page.dart | 6 +- .../local_asset_repository_test.dart | 67 ++++++++++++ .../remote_asset_repository_test.dart | 67 ++++++++++++ .../timeline_repository_test.dart | 44 ++++++++ .../trashed_local_asset_repository_test.dart | 100 ++++++++++++++++++ mobile/test/medium/repository_context.dart | 46 ++++++++ 11 files changed, 359 insertions(+), 22 deletions(-) create mode 100644 mobile/test/medium/repositories/remote_asset_repository_test.dart create mode 100644 mobile/test/medium/repositories/trashed_local_asset_repository_test.dart diff --git a/mobile/lib/domain/services/asset.service.dart b/mobile/lib/domain/services/asset.service.dart index f35c962ff0..9cbdf1bdfe 100644 --- a/mobile/lib/domain/services/asset.service.dart +++ b/mobile/lib/domain/services/asset.service.dart @@ -36,8 +36,8 @@ class AssetService { return _localRepository.getByChecksum(checksum); } - Future getRemoteAssetByChecksum(String checksum) { - return _remoteRepository.getByChecksum(checksum); + Future> getAllRemoteAssetDebugByChecksum(String checksum) { + return _remoteRepository.getAllDebugForChecksum(checksum); } Future getRemoteAsset(String id) { diff --git a/mobile/lib/infrastructure/repositories/local_asset.repository.dart b/mobile/lib/infrastructure/repositories/local_asset.repository.dart index c34d2c4697..8396d6d2a6 100644 --- a/mobile/lib/infrastructure/repositories/local_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_asset.repository.dart @@ -24,13 +24,21 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository { const DriftLocalAssetRepository(this._db) : super(_db); SingleOrNullSelectable _assetSelectable(String id) { - final query = _db.localAssetEntity.select().addColumns([_db.remoteAssetEntity.id]).join([ - leftOuterJoin( - _db.remoteAssetEntity, - _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum), - useColumns: false, - ), - ])..where(_db.localAssetEntity.id.equals(id)); + final query = + _db.localAssetEntity.select().addColumns([_db.remoteAssetEntity.id]).join([ + leftOuterJoin( + _db.remoteAssetEntity, + _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum) & + _db.remoteAssetEntity.ownerId.isInQuery( + _db.selectOnly(_db.authUserEntity) + ..addColumns([_db.authUserEntity.id]) + ..limit(1), + ), + useColumns: false, + ), + ]) + ..where(_db.localAssetEntity.id.equals(id)) + ..limit(1); return query.map((row) { final asset = row.readTable(_db.localAssetEntity).toDto(); diff --git a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart index e97b05465b..d18284b636 100644 --- a/mobile/lib/infrastructure/repositories/remote_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_asset.repository.dart @@ -59,10 +59,10 @@ class RemoteAssetRepository extends DriftDatabaseRepository { return _assetSelectable(id).getSingleOrNull(); } - Future getByChecksum(String checksum) { + Future> getAllDebugForChecksum(String checksum) { final query = _db.remoteAssetEntity.select()..where((row) => row.checksum.equals(checksum)); - return query.map((row) => row.toDto()).getSingleOrNull(); + return query.map((row) => row.toDto()).get(); } Future> getStackChildren(RemoteAsset asset) { diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index 6a9fbdb204..82ad38c80f 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -139,11 +139,6 @@ class DriftTimelineRepository extends DriftDatabaseRepository { _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id), useColumns: false, ), - leftOuterJoin( - _db.remoteAssetEntity, - _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum), - useColumns: false, - ), ]) ..addColumns([assetCountExp, dateExp]) ..where(_db.localAlbumAssetEntity.albumId.equals(albumId)) @@ -167,7 +162,12 @@ class DriftTimelineRepository extends DriftDatabaseRepository { ), leftOuterJoin( _db.remoteAssetEntity, - _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum), + _db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum) & + _db.remoteAssetEntity.ownerId.isInQuery( + _db.selectOnly(_db.authUserEntity) + ..addColumns([_db.authUserEntity.id]) + ..limit(1), + ), useColumns: false, ), ]) diff --git a/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart b/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart index e31b47a9fc..0f50776e4e 100644 --- a/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart +++ b/mobile/lib/infrastructure/repositories/trashed_local_asset.repository.dart @@ -16,6 +16,11 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { const DriftTrashedLocalAssetRepository(this._db) : super(_db); + /// Matches remote_asset_entity rows owned by the current user. The asset is unique over (owner, checksum), + /// so partners can have a duplicate checksum + Expression get _ownedByCurrentUser => + _db.remoteAssetEntity.ownerId.isInQuery(_db.selectOnly(_db.authUserEntity)..addColumns([_db.authUserEntity.id])); + Future updateHashes(Map hashes) { if (hashes.isEmpty) { return Future.value(); @@ -45,7 +50,7 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { await (_db.select(_db.trashedLocalAssetEntity).join([ innerJoin( _db.remoteAssetEntity, - _db.remoteAssetEntity.checksum.equalsExp(_db.trashedLocalAssetEntity.checksum), + _db.remoteAssetEntity.checksum.equalsExp(_db.trashedLocalAssetEntity.checksum) & _ownedByCurrentUser, ), ])..where( _db.trashedLocalAssetEntity.source.equalsValue(TrashOrigin.remoteSync) & @@ -273,7 +278,7 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository { innerJoin(_db.localAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id)), leftOuterJoin( _db.remoteAssetEntity, - _db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum), + _db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum) & _ownedByCurrentUser, ), ])..where( _db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected) & diff --git a/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart b/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart index cee0cdc334..d50e4b190e 100644 --- a/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart +++ b/mobile/lib/presentation/pages/drift_asset_troubleshoot.page.dart @@ -280,8 +280,8 @@ class _RemoteAssetSection extends ConsumerWidget { return const SizedBox.shrink(); } - return FutureBuilder( - future: assetService.getRemoteAssetByChecksum(asset.checksum!), + return FutureBuilder>( + future: assetService.getAllRemoteAssetDebugByChecksum(asset.checksum!), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const _PropertySectionCard( @@ -297,7 +297,7 @@ class _RemoteAssetSection extends ConsumerWidget { ); } - final remoteAsset = snapshot.data; + final remoteAsset = snapshot.data?.firstOrNull; if (remoteAsset == null) { return _PropertySectionCard( diff --git a/mobile/test/medium/repositories/local_asset_repository_test.dart b/mobile/test/medium/repositories/local_asset_repository_test.dart index d92b1c0184..2376445d1a 100644 --- a/mobile/test/medium/repositories/local_asset_repository_test.dart +++ b/mobile/test/medium/repositories/local_asset_repository_test.dart @@ -19,6 +19,73 @@ void main() { await ctx.dispose(); }); + group('get', () { + late String userId; + + setUp(() async { + final user = await ctx.newUser(); + userId = user.id; + // Owner-scoped queries resolve the current user via authUserEntity. + await ctx.newAuthUser(id: userId); + }); + + test('allows the same checksum to exist for multiple owners (#29973)', () async { + const checksum = 'some-shared-checksum'; + final mine = await ctx.newRemoteAsset(ownerId: userId, checksum: checksum); + final partner = await ctx.newUser(); + await ctx.newRemoteAsset(ownerId: partner.id, checksum: checksum); + final local = await ctx.newLocalAsset(checksum: checksum); + + final result = await sut.get(local.id); + + expect(result, isNotNull); + expect(result!.id, local.id); + // We must explicitly get OUR asset, not the partner's + expect(result.remoteId, mine.id); + }); + + test('reports local-only when only a partner has a remote copy (#29973)', () async { + // The current user has NOT uploaded this file; only a partner owns an identical-checksum remote asset + const checksum = 'partner-only'; + final partner = await ctx.newUser(); + await ctx.newRemoteAsset(ownerId: partner.id, checksum: checksum); + final local = await ctx.newLocalAsset(checksum: checksum); + + final result = await sut.get(local.id); + + expect(result, isNotNull); + expect(result!.remoteId, isNull); + expect(result.storage, AssetState.local); + }); + + test('allows the current user to have access to multiple remote rows for one checksum (#29973)', () async { + // A single user can have their own remote asset, a partner's remote asset, and a local asset all with the same checksum + const checksum = 'multi-library'; + final partner = await ctx.newUser(); + await ctx.newRemoteAsset(ownerId: partner.id, checksum: checksum); + await ctx.newRemoteAsset(ownerId: userId, checksum: checksum); + final local = await ctx.newLocalAsset(checksum: checksum); + + final result = await sut.get(local.id); + + expect(result, isNotNull); + expect(result!.id, local.id); + expect(result.remoteId, isNotNull); + }); + + test('attaches remoteId to local asset automatically in simple scenarios', () async { + const checksum = 'simple'; + final remote = await ctx.newRemoteAsset(ownerId: userId, checksum: checksum); + final local = await ctx.newLocalAsset(checksum: checksum); + + final result = await sut.get(local.id); + + expect(result, isNotNull); + expect(result!.remoteId, remote.id); + expect(result.storage, AssetState.merged); + }); + }); + group('getRemovalCandidates', () { final cutoffDate = DateTime(2024, 1, 1); final beforeCutoff = DateTime(2023, 12, 31); diff --git a/mobile/test/medium/repositories/remote_asset_repository_test.dart b/mobile/test/medium/repositories/remote_asset_repository_test.dart new file mode 100644 index 0000000000..f534355d99 --- /dev/null +++ b/mobile/test/medium/repositories/remote_asset_repository_test.dart @@ -0,0 +1,67 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; + +import '../repository_context.dart'; + +void main() { + late MediumRepositoryContext ctx; + late RemoteAssetRepository sut; + + setUp(() { + ctx = MediumRepositoryContext(); + sut = RemoteAssetRepository(ctx.db); + }); + + tearDown(() async { + await ctx.dispose(); + }); + + group('getByChecksum', () { + late String userId; + + setUp(() async { + final user = await ctx.newUser(); + userId = user.id; + await ctx.newAuthUser(id: userId); + }); + + test('returns all assets when a partner shares the checksum', () async { + const checksum = 'shared-partner-checksum'; + final mine = await ctx.newRemoteAsset(ownerId: userId, checksum: checksum); + final partner = await ctx.newUser(); + final theirs = await ctx.newRemoteAsset(ownerId: partner.id, checksum: checksum); + + final result = await sut.getAllDebugForChecksum(checksum); + final mineResult = result.firstWhere((asset) => asset.id == mine.id); + final theirResult = result.firstWhere((asset) => asset.id == theirs.id); + + expect(result, isNotEmpty); + expect(mineResult.id, mine.id); + expect(mineResult.ownerId, userId); + + expect(theirResult.id, theirs.id); + expect(theirResult.ownerId, partner.id); + }); + + test('returns partner asset only if there is no matching user asset', () async { + const checksum = 'partner-only'; + final partner = await ctx.newUser(); + final theirs = await ctx.newRemoteAsset(ownerId: partner.id, checksum: checksum); + + final result = await sut.getAllDebugForChecksum(checksum); + + expect(result.length, 1); + expect(result[0].id, theirs.id); + }); + + test('returns the current user\'s asset', () async { + const checksum = 'simple'; + final remote = await ctx.newRemoteAsset(ownerId: userId, checksum: checksum); + + final result = await sut.getAllDebugForChecksum(checksum); + + expect(result.length, 1); + expect(result[0].id, remote.id); + }); + }); +} diff --git a/mobile/test/medium/repositories/timeline_repository_test.dart b/mobile/test/medium/repositories/timeline_repository_test.dart index d78d9b1ef7..502a65d45b 100644 --- a/mobile/test/medium/repositories/timeline_repository_test.dart +++ b/mobile/test/medium/repositories/timeline_repository_test.dart @@ -102,4 +102,48 @@ void main() { expect(remote.localId, local.id); }); }); + + group('localAlbum assets', () { + late String userId; + late String otherUserId; + + setUp(() async { + final user = await ctx.newUser(); + userId = user.id; + await ctx.newAuthUser(id: userId); + final other = await ctx.newUser(); + otherUserId = other.id; + }); + + test('does not duplicate assets when a partner shares the checksum', () async { + const checksum = 'shared-partner-checksum'; + final album = await ctx.newLocalAlbum(); + final local = await ctx.newLocalAsset(checksum: checksum); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: local.id); + final myRemote = await ctx.newRemoteAsset(ownerId: userId, checksum: checksum); + await ctx.newRemoteAsset(ownerId: otherUserId, checksum: checksum); + + final assets = await sut.localAlbum(album.id, .day).assetSource(0, 10); + + expect(assets, hasLength(1)); + final asset = assets.single as LocalAsset; + expect(asset.id, local.id); + // Must resolve the current user's remote id + expect(asset.remoteId, myRemote.id); + }); + + test('bucket count ignores a partner sharing the checksum', () async { + const checksum = 'shared-partner-checksum'; + final album = await ctx.newLocalAlbum(); + final local = await ctx.newLocalAsset(checksum: checksum); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: local.id); + await ctx.newRemoteAsset(ownerId: userId, checksum: checksum); + await ctx.newRemoteAsset(ownerId: otherUserId, checksum: checksum); + + final buckets = await sut.localAlbum(album.id, .day).bucketSource().first; + + expect(buckets, hasLength(1)); + expect(buckets.single.assetCount, 1); + }); + }); } diff --git a/mobile/test/medium/repositories/trashed_local_asset_repository_test.dart b/mobile/test/medium/repositories/trashed_local_asset_repository_test.dart new file mode 100644 index 0000000000..0af09715c7 --- /dev/null +++ b/mobile/test/medium/repositories/trashed_local_asset_repository_test.dart @@ -0,0 +1,100 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; + +import '../repository_context.dart'; + +void main() { + late MediumRepositoryContext ctx; + late DriftTrashedLocalAssetRepository sut; + + setUp(() { + ctx = MediumRepositoryContext(); + sut = DriftTrashedLocalAssetRepository(ctx.db); + }); + + tearDown(() async { + await ctx.dispose(); + }); + + group('getToRestore', () { + late String userId; + + setUp(() async { + final user = await ctx.newUser(); + userId = user.id; + await ctx.newAuthUser(id: userId); + }); + + test('does not restore based on a partner\'s live remote copy', () async { + const checksum = 'shared-partner-checksum'; + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final trashed = await ctx.newTrashedLocalAsset(albumId: album.id, checksum: checksum); + + // Current user's own remote copy is deleted; only a partner has an active copy. + await ctx.newRemoteAsset(ownerId: userId, checksum: checksum, deletedAt: DateTime(2020, 1, 1)); + final partner = await ctx.newUser(); + await ctx.newRemoteAsset(ownerId: partner.id, checksum: checksum); + + final result = await sut.getToRestore(); + + final ids = result.map((a) => a.id); + expect(ids, isNot(contains(trashed.id))); + }); + + test('restores when the current user\'s own remote copy is live', () async { + const checksum = 'my-live-copy'; + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final trashed = await ctx.newTrashedLocalAsset(albumId: album.id, checksum: checksum); + await ctx.newRemoteAsset(ownerId: userId, checksum: checksum); + + final result = await sut.getToRestore(); + + final ids = result.map((a) => a.id); + expect(ids, contains(trashed.id)); + }); + }); + + group('getToTrash', () { + late String userId; + + setUp(() async { + final user = await ctx.newUser(); + userId = user.id; + await ctx.newAuthUser(id: userId); + }); + + Future addLocalAssetToBackupAlbum(String checksum) async { + final album = await ctx.newLocalAlbum(backupSelection: BackupSelection.selected); + final local = await ctx.newLocalAsset(checksum: checksum); + await ctx.newLocalAlbumAsset(albumId: album.id, assetId: local.id); + return local.id; + } + + test('does not trash when only a partner\'s remote copy is deleted', () async { + const checksum = 'shared-partner-checksum'; + final localId = await addLocalAssetToBackupAlbum(checksum); + + // Current user's own remote copy is live but a partner deleted theirs + await ctx.newRemoteAsset(ownerId: userId, checksum: checksum); + final partner = await ctx.newUser(); + await ctx.newRemoteAsset(ownerId: partner.id, checksum: checksum, deletedAt: DateTime(2020, 1, 1)); + + final result = await sut.getToTrash(); + + final ids = result.values.expand((assets) => assets).map((a) => a.id); + expect(ids, isNot(contains(localId))); + }); + + test('trashes when the current user\'s own remote copy is deleted', () async { + const checksum = 'my-deleted-copy'; + final localId = await addLocalAssetToBackupAlbum(checksum); + await ctx.newRemoteAsset(ownerId: userId, checksum: checksum, deletedAt: DateTime(2020, 1, 1)); + + final result = await sut.getToTrash(); + + final ids = result.values.expand((assets) => assets).map((a) => a.id); + expect(ids, contains(localId)); + }); + }); +} diff --git a/mobile/test/medium/repository_context.dart b/mobile/test/medium/repository_context.dart index ed06774e82..09b8e2c7eb 100644 --- a/mobile/test/medium/repository_context.dart +++ b/mobile/test/medium/repository_context.dart @@ -6,6 +6,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; @@ -18,6 +19,8 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity. import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset_cloud_id.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/utils/option.dart'; @@ -72,6 +75,22 @@ class MediumRepositoryContext { ); } + /// Seeds a user into `authUserEntity` as the currently authenticated user + Future newAuthUser({String? id, String? email, bool? isAdmin, AvatarColor? avatarColor}) async { + id ??= TestUtils.uuid(); + return db + .into(db.authUserEntity) + .insertReturning( + AuthUserEntityCompanion( + id: .new(id), + email: .new(email ?? '$id@test.com'), + name: .new('user_$id'), + isAdmin: .new(isAdmin ?? false), + avatarColor: .new(avatarColor ?? TestUtils.randElement(AvatarColor.values)), + ), + ); + } + Future newPartner({required String sharedById, required String sharedWithId, bool? inTimeline}) { return db .into(db.partnerEntity) @@ -286,6 +305,33 @@ class MediumRepositoryContext { ); } + /// Seeds a trashed local asset into `trashedLocalAssetEntity` + Future newTrashedLocalAsset({ + String? id, + required String albumId, + String? checksum, + TrashOrigin? source, + AssetType? type, + DateTime? createdAt, + bool? isFavorite, + }) async { + id ??= TestUtils.uuid(); + return db + .into(db.trashedLocalAssetEntity) + .insertReturning( + TrashedLocalAssetEntityCompanion( + id: .new(id), + albumId: .new(albumId), + name: .new('trashed_$id.jpg'), + type: .new(type ?? .image), + checksum: .new(checksum), + source: .new(source ?? TrashOrigin.remoteSync), + isFavorite: .new(isFavorite ?? false), + createdAt: .new(TestUtils.date(createdAt)), + ), + ); + } + Future newLocalAlbum({ String? id, String? name, From 5b0a324b68e1b795c527b9f64f59ec26d30fc2c3 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Sat, 1 Aug 2026 08:06:40 +0530 Subject: [PATCH 31/81] feat: restore action (#29374) * refactor: existing actions to new structure # Conflicts: # mobile/lib/presentation/actions/action.dart # mobile/lib/presentation/actions/action.widget.dart # mobile/lib/presentation/actions/asset_debug.action.dart # mobile/lib/presentation/actions/favorite.action.dart # mobile/lib/presentation/actions/partner.action.dart # mobile/test/unit/presentation/actions/favorite_action_test.dart * rename to actionitem * review changes * refactor: mobile restore action --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../presentation/actions/favorite.action.dart | 4 +- .../presentation/actions/restore.action.dart | 47 ++++++++++++ .../restore_action_button.widget.dart | 56 -------------- .../restore_trash_action_button.widget.dart | 43 ----------- .../asset_viewer/bottom_bar.widget.dart | 9 +-- .../trash_bottom_sheet.widget.dart | 5 +- .../infrastructure/action.provider.dart | 11 --- mobile/lib/services/action.service.dart | 5 -- mobile/lib/utils/action_button.utils.dart | 8 +- .../services/sync_stream_service_test.dart | 1 - .../actions/restore_action_test.dart | 75 +++++++++++++++++++ 11 files changed, 133 insertions(+), 131 deletions(-) create mode 100644 mobile/lib/presentation/actions/restore.action.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/restore_action_button.widget.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/restore_trash_action_button.widget.dart create mode 100644 mobile/test/unit/presentation/actions/restore_action_test.dart diff --git a/mobile/lib/presentation/actions/favorite.action.dart b/mobile/lib/presentation/actions/favorite.action.dart index 9ab3770163..9b7568610c 100644 --- a/mobile/lib/presentation/actions/favorite.action.dart +++ b/mobile/lib/presentation/actions/favorite.action.dart @@ -47,12 +47,12 @@ class FavoriteAction extends AssetActionBuilder { final message = shouldFavorite ? context.t.favorite_action_prompt(count: assetIds.length) : context.t.unfavorite_action_prompt(count: assetIds.length); - final assertService = ref.read(assetServiceProvider); + final assetService = ref.read(assetServiceProvider); final toastService = ref.read(toastServiceProvider); final clearSelection = ref.read(clearSelectionProvider(source)); try { - await assertService.update(assetIds, isFavorite: .some(shouldFavorite)); + await assetService.update(assetIds, isFavorite: .some(shouldFavorite)); toastService.success(message); clearSelection(); } catch (error, stack) { diff --git a/mobile/lib/presentation/actions/restore.action.dart b/mobile/lib/presentation/actions/restore.action.dart new file mode 100644 index 0000000000..0a2f34abf8 --- /dev/null +++ b/mobile/lib/presentation/actions/restore.action.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; + +final _stateProvider = Provider.family.autoDispose?, ActionSource>((ref, source) { + final assets = ref.watch(ownedAssetsActionProvider(source)); + final assetIds = assets.trashed().map((asset) => asset.id).toList(growable: false); + return assetIds.isEmpty ? null : assetIds; +}); + +class RestoreAction extends AssetActionBuilder { + const RestoreAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + if (!ref.watch(_stateProvider(source).select((state) => state != null))) { + return null; + } + + return .new(icon: Icons.history_rounded, label: context.t.restore, onAction: () => _restore(context, ref)); + } + + Future _restore(BuildContext context, WidgetRef ref) async { + final assetIds = ref.read(_stateProvider(source)); + if (assetIds == null) { + return; + } + + final message = context.t.assets_restored_count(count: assetIds.length); + final assetService = ref.read(assetServiceProvider); + final toastService = ref.read(toastServiceProvider); + final clearSelection = ref.read(clearSelectionProvider(source)); + + try { + await assetService.restoreTrash(assetIds); + toastService.success(message); + clearSelection(); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to restore assets"); + } + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/restore_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/restore_action_button.widget.dart deleted file mode 100644 index 9270ce8351..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/restore_action_button.widget.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter/material.dart'; -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/events.model.dart'; -import 'package:immich_mobile/domain/utils/event_stream.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class RestoreActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const RestoreActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - Future _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final result = await ref.read(actionProvider.notifier).restoreTrash(source); - ref.read(multiSelectProvider.notifier).reset(); - - if (source == ActionSource.viewer) { - EventStream.shared.emit(const ViewerReloadAssetEvent()); - } - - if (!context.mounted) { - return; - } - - final successMessage = 'assets_restored_count'.t(context: context, args: {'count': result.count.toString()}); - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.history_rounded, - label: 'restore'.t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - maxWidth: 100.0, - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/restore_trash_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/restore_trash_action_button.widget.dart deleted file mode 100644 index f6cd26c189..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/restore_trash_action_button.widget.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class RestoreTrashActionButton extends ConsumerWidget { - final ActionSource source; - - const RestoreTrashActionButton({super.key, required this.source}); - - Future _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final result = await ref.read(actionProvider.notifier).restoreTrash(source); - ref.read(multiSelectProvider.notifier).reset(); - if (!context.mounted) { - return; - } - - final successMessage = 'assets_restored_count'.t(context: context, args: {'count': result.count.toString()}); - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return TextButton.icon( - icon: const Icon(Icons.history_rounded), - label: Text('restore'.t(), style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart index 53a7aee700..14aa768626 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -4,12 +4,13 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/restore.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/restore_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/ocr_toggle_button.widget.dart'; @@ -43,10 +44,8 @@ class ViewerBottomBar extends ConsumerWidget { final originalTheme = context.themeData; final actions = [ - if (isInTrash && isOwner && asset.hasRemote) - const RestoreActionButton(source: ActionSource.viewer) - else - const ShareActionButton(source: ActionSource.viewer), + const ActionColumnButton(action: RestoreAction(source: .viewer)), + const ShareActionButton(source: .viewer), if (!isInLockedView) ...[ if (!isInTrash) ...[ diff --git a/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart index c96e680966..4e438884b3 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart @@ -2,8 +2,9 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/restore.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/restore_trash_action_button.widget.dart'; class TrashBottomBar extends ConsumerWidget { const TrashBottomBar({super.key}); @@ -21,7 +22,7 @@ class TrashBottomBar extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ DeleteTrashActionButton(source: ActionSource.timeline), - RestoreTrashActionButton(source: ActionSource.timeline), + ActionColumnButton(action: RestoreAction(source: .timeline)), ], ), ), diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index d4cd39bbd3..2958d761c0 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -194,17 +194,6 @@ class ActionNotifier extends Notifier { } } - Future restoreTrash(ActionSource source) async { - final ids = _getOwnedRemoteIdsForSource(source); - try { - await _service.restoreTrash(ids); - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to restore trash assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - Future emptyTrash(String userId) async { try { final count = await _service.emptyTrash(userId); diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index dd1b3e8496..b1219383f5 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -98,11 +98,6 @@ class ActionService { await _remoteAssetRepository.trash(remoteIds); } - Future restoreTrash(List ids) async { - await _assetApiRepository.restoreTrash(ids); - await _remoteAssetRepository.restoreTrash(ids); - } - Future emptyTrash(String userId) async { final count = await _assetApiRepository.emptyTrash(); await _remoteAssetRepository.emptyTrash(userId); diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 4219e0aed7..a838333475 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; +import 'package:immich_mobile/presentation/actions/restore.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart'; @@ -21,7 +22,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_f import 'package:immich_mobile/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/restore_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; @@ -208,11 +208,7 @@ enum ActionButtonType { ), ActionButtonType.download => DownloadActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), ActionButtonType.trash => TrashActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), - ActionButtonType.restoreTrash => RestoreActionButton( - source: context.source, - iconOnly: iconOnly, - menuItem: menuItem, - ), + ActionButtonType.restoreTrash => ActionMenuItem(action: RestoreAction(source: context.source)), ActionButtonType.deletePermanent => DeletePermanentActionButton( source: context.source, iconOnly: iconOnly, diff --git a/mobile/test/domain/services/sync_stream_service_test.dart b/mobile/test/domain/services/sync_stream_service_test.dart index ac81513d96..5cd4104f91 100644 --- a/mobile/test/domain/services/sync_stream_service_test.dart +++ b/mobile/test/domain/services/sync_stream_service_test.dart @@ -36,7 +36,6 @@ class _AbortCallbackWrapper { class _MockAbortCallbackWrapper extends Mock implements _AbortCallbackWrapper {} - void main() { late SyncStreamService sut; late SyncStreamRepository mockSyncStreamRepo; diff --git a/mobile/test/unit/presentation/actions/restore_action_test.dart b/mobile/test/unit/presentation/actions/restore_action_test.dart new file mode 100644 index 0000000000..d7992850e5 --- /dev/null +++ b/mobile/test/unit/presentation/actions/restore_action_test.dart @@ -0,0 +1,75 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/restore.action.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../service.mocks.dart'; +import '../../factories/remote_asset_factory.dart'; +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + late MockAssetService assetService; + + setUp(() async { + context = await PresentationContext.create(); + assetService = context.service.asset.service; + }); + + tearDown(() { + context.dispose(); + }); + + RemoteAsset owned({bool trashed = true}) => + RemoteAssetFactory.create(ownerId: context.currentUser.id, deletedAt: trashed ? DateTime(2020) : null); + + Future pumpRestore(WidgetTester tester, Set selection) => + tester.pumpTestAction(context, const RestoreAction(source: .timeline), overrides: context.selected(selection)); + + group('RestoreAction', () { + testWidgets('restores the eligible owned trashed assets', (tester) async { + final asset = owned(); + + await pumpRestore(tester, {asset}); + + verify(() => assetService.restoreTrash([asset.id])).called(1); + }); + + testWidgets('ignores assets owned by someone else', (tester) async { + final mine = owned(); + final theirs = RemoteAssetFactory.create(deletedAt: DateTime(2020)); + + await pumpRestore(tester, {mine, theirs}); + + verify(() => assetService.restoreTrash([mine.id])).called(1); + }); + + testWidgets('skips owned assets that are not trashed', (tester) async { + final trashed = owned(); + final live = owned(trashed: false); + + await pumpRestore(tester, {trashed, live}); + + verify(() => assetService.restoreTrash([trashed.id])).called(1); + }); + + testWidgets('clears the selection once the restore succeeds', (tester) async { + await pumpRestore(tester, {owned()}); + await tester.pumpAndSettle(); + + expect(find.byType(ImmichIconButton), findsNothing, reason: 'an empty selection hides the action'); + }); + + testWidgets('is hidden when no owned asset is trashed', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: RestoreAction(source: .timeline)), + overrides: context.selected({owned(trashed: false)}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); +} From 208b7cf9cac5826ad02d56539d86cd37de7f71cc Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Sat, 1 Aug 2026 08:06:40 +0530 Subject: [PATCH 32/81] feat: stack action (#29370) refactor: mobile stack action Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../presentation/actions/favorite.action.dart | 2 +- .../presentation/actions/stack.action.dart | 72 ++++++++++++++ .../stack_action_button.widget.dart | 50 ---------- .../unstack_action_button.widget.dart | 48 ---------- .../archive_bottom_sheet.widget.dart | 6 +- .../favorite_bottom_sheet.widget.dart | 6 +- .../general_bottom_sheet.widget.dart | 6 +- .../remote_album_bottom_sheet.widget.dart | 6 +- .../infrastructure/action.provider.dart | 48 ---------- .../timeline/multiselect.provider.dart | 2 - mobile/lib/services/action.service.dart | 10 -- mobile/lib/utils/action_button.utils.dart | 4 +- .../actions/stack_action_test.dart | 94 +++++++++++++++++++ 13 files changed, 177 insertions(+), 177 deletions(-) create mode 100644 mobile/lib/presentation/actions/stack.action.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/stack_action_button.widget.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart create mode 100644 mobile/test/unit/presentation/actions/stack_action_test.dart diff --git a/mobile/lib/presentation/actions/favorite.action.dart b/mobile/lib/presentation/actions/favorite.action.dart index 9b7568610c..17c841b745 100644 --- a/mobile/lib/presentation/actions/favorite.action.dart +++ b/mobile/lib/presentation/actions/favorite.action.dart @@ -43,7 +43,7 @@ class FavoriteAction extends AssetActionBuilder { return; } - final _State(:shouldFavorite, :assetIds) = state; + final (:shouldFavorite, :assetIds) = state; final message = shouldFavorite ? context.t.favorite_action_prompt(count: assetIds.length) : context.t.unfavorite_action_prompt(count: assetIds.length); diff --git a/mobile/lib/presentation/actions/stack.action.dart b/mobile/lib/presentation/actions/stack.action.dart new file mode 100644 index 0000000000..9697dc02be --- /dev/null +++ b/mobile/lib/presentation/actions/stack.action.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; + +typedef _State = ({bool shouldStack, List assetIds, List stackIds}); + +final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, source) { + final assets = ref.watch(ownedAssetsActionProvider(source)); + final shouldStack = assets.stacked(isStacked: false).isNotEmpty; + // Stacking needs at least two assets; unstacking needs at least one stack. + if (shouldStack ? assets.elementAtOrNull(1) == null : assets.isEmpty) { + return null; + } + + return ( + shouldStack: shouldStack, + assetIds: assets.map((asset) => asset.id).toList(growable: false), + stackIds: assets.map((asset) => asset.stackId).nonNulls.toList(growable: false), + ); +}); + +class StackAction extends AssetActionBuilder { + const StackAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + final shouldStack = ref.watch(_stateProvider(source).select((state) => state?.shouldStack)); + if (shouldStack == null) { + return null; + } + + return .new( + icon: shouldStack ? Icons.filter_none_rounded : Icons.layers_clear_outlined, + label: shouldStack ? context.t.stack : context.t.unstack, + onAction: () => _stack(context, ref), + ); + } + + Future _stack(BuildContext context, WidgetRef ref) async { + final state = ref.read(_stateProvider(source)); + if (state == null) { + return; + } + + final (:shouldStack, :assetIds, :stackIds) = state; + final message = shouldStack + ? context.t.stacked_assets_count(count: assetIds.length) + : context.t.unstacked_assets_count(count: assetIds.length); + final assetService = ref.read(assetServiceProvider); + final userId = ref.read(authUserProvider).id; + final toastService = ref.read(toastServiceProvider); + final clearSelection = ref.read(clearSelectionProvider(source)); + + try { + if (shouldStack) { + await assetService.stack(userId, assetIds); + } else { + await assetService.unstack(stackIds); + } + toastService.success(message); + clearSelection(); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to update the stack for assets"); + } + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/stack_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/stack_action_button.widget.dart deleted file mode 100644 index 026268fe52..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/stack_action_button.widget.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class StackActionButton extends ConsumerWidget { - final ActionSource source; - - const StackActionButton({super.key, required this.source}); - - Future _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final user = ref.watch(currentUserProvider); - if (user == null) { - throw Exception('User must be logged in to access stack action'); - } - - final result = await ref.read(actionProvider.notifier).stack(user.id, source); - ref.read(multiSelectProvider.notifier).reset(); - if (!context.mounted) { - return; - } - - final successMessage = 'stack_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.filter_none_rounded, - label: "stack".t(context: context), - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart deleted file mode 100644 index 47cdfe9b5f..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/unstack_action_button.widget.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class UnStackActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const UnStackActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - Future _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final result = await ref.read(actionProvider.notifier).unStack(source); - ref.read(multiSelectProvider.notifier).reset(); - if (!context.mounted) { - return; - } - - final successMessage = 'unstack_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.layers_clear_outlined, - label: "unstack".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart index 85fa8b4563..7c3f72756c 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; +import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; @@ -13,10 +14,8 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_ import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; @@ -93,8 +92,7 @@ class _ArchiveBottomSheetState extends ConsumerState { const EditDateTimeActionButton(source: ActionSource.timeline), const EditLocationActionButton(source: ActionSource.timeline), const MoveToLockFolderActionButton(source: ActionSource.timeline), - if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline), - if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: StackAction(source: .timeline)), ], if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline), ], diff --git a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart index 6447c5ccf8..334d3a4309 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart @@ -6,6 +6,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; +import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; @@ -15,9 +16,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_ import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; @@ -93,8 +92,7 @@ class FavoriteBottomSheet extends ConsumerWidget { const EditDateTimeActionButton(source: ActionSource.timeline), const EditLocationActionButton(source: ActionSource.timeline), const MoveToLockFolderActionButton(source: ActionSource.timeline), - if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline), - if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: StackAction(source: .timeline)), ], if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline), ], diff --git a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart index 2f1ffe4cbc..a35223137a 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; +import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; @@ -16,9 +17,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_ import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; @@ -102,8 +101,7 @@ class _GeneralBottomSheetState extends ConsumerState { const EditDateTimeActionButton(source: ActionSource.timeline), const EditLocationActionButton(source: ActionSource.timeline), const MoveToLockFolderActionButton(source: ActionSource.timeline), - if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline), - if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: StackAction(source: .timeline)), if (multiselect.onlyLocal || multiselect.hasMerged) const DeleteActionButton(source: ActionSource.timeline), ], if (multiselect.onlyLocal || multiselect.hasMerged) diff --git a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart index f6cbc5eac9..608a864101 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; +import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; @@ -16,9 +17,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_al import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; @@ -107,8 +106,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState const EditDateTimeActionButton(source: ActionSource.timeline), const EditLocationActionButton(source: ActionSource.timeline), const MoveToLockFolderActionButton(source: ActionSource.timeline), - if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline), - if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: StackAction(source: .timeline)), ], ], if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline), diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index 2958d761c0..11eda6d3de 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -7,12 +7,10 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset_edit.model.dart'; -import 'package:immich_mobile/domain/services/asset.service.dart'; import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart' show assetExifProvider; import 'package:immich_mobile/providers/infrastructure/tag.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; @@ -53,7 +51,6 @@ class ActionNotifier extends Notifier { final Logger _logger = Logger('ActionNotifier'); late ActionService _service; late ForegroundUploadService _foregroundUploadService; - late AssetService _assetService; ActionNotifier() : super(); @@ -61,7 +58,6 @@ class ActionNotifier extends Notifier { void build() { _foregroundUploadService = ref.watch(foregroundUploadServiceProvider); _service = ref.watch(actionServiceProvider); - _assetService = ref.watch(assetServiceProvider); } List _getRemoteIdsForSource(ActionSource source) { @@ -91,21 +87,6 @@ class ActionNotifier extends Notifier { return _getAssets(source).whereType().ownedAssets(ownerId).toIds().toList(growable: false); } - List _getOwnedRemoteAssetsForSource(ActionSource source) { - final ownerId = ref.read(currentUserProvider)?.id; - return _getIdsForSource(source).ownedAssets(ownerId).toList(); - } - - Iterable _getIdsForSource(ActionSource source) { - final Set assets = _getAssets(source); - return switch (T) { - const (RemoteAsset) => assets.whereType(), - const (LocalAsset) => assets.whereType(), - _ => const [], - } - as Iterable; - } - Set _getAssets(ActionSource source) { return switch (source) { ActionSource.timeline => ref.read(multiSelectProvider).selectedAssets, @@ -438,35 +419,6 @@ class ActionNotifier extends Notifier { } } - Future stack(String userId, ActionSource source) async { - final ids = _getOwnedRemoteIdsForSource(source); - try { - await _service.stack(userId, ids); - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to stack assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - - Future unStack(ActionSource source) async { - final assets = _getOwnedRemoteAssetsForSource(source); - try { - await _service.unStack(assets.map((e) => e.stackId).nonNulls.toList()); - if (source == ActionSource.viewer) { - final updatedParent = await _assetService.getRemoteAsset(assets.first.id); - if (updatedParent != null) { - ref.read(assetViewerProvider.notifier).setAsset(updatedParent); - } - } - - return ActionResult(count: assets.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to unstack assets', error, stack); - return ActionResult(count: assets.length, success: false); - } - } - Future shareAssets( ActionSource source, BuildContext context, { diff --git a/mobile/lib/providers/timeline/multiselect.provider.dart b/mobile/lib/providers/timeline/multiselect.provider.dart index cb053e0041..64edd4cd3f 100644 --- a/mobile/lib/providers/timeline/multiselect.provider.dart +++ b/mobile/lib/providers/timeline/multiselect.provider.dart @@ -22,8 +22,6 @@ class MultiSelectState { bool get hasRemote => selectedAssets.any((asset) => asset.storage == AssetState.remote || asset.storage == AssetState.merged); - bool get hasStacked => selectedAssets.any((asset) => asset is RemoteAsset && asset.stackId != null); - bool get hasMerged => selectedAssets.any((asset) => asset.storage == AssetState.merged); bool get onlyLocal => selectedAssets.any((asset) => asset.storage == AssetState.local); diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index b1219383f5..ad4f3098b8 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -261,16 +261,6 @@ class ActionService { return _tagService.bulkTagAssets(remoteIds, selectedTagIds.toList()); } - Future stack(String userId, List remoteIds) async { - final stack = await _assetApiRepository.stack(remoteIds); - await _remoteAssetRepository.stack(userId, stack); - } - - Future unStack(List stackIds) async { - await _remoteAssetRepository.unStack(stackIds); - await _assetApiRepository.unStack(stackIds); - } - Future shareAssets( List assets, BuildContext context, { diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index a838333475..8b51a5ea6a 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -10,6 +10,7 @@ import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; import 'package:immich_mobile/presentation/actions/restore.action.dart'; +import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart'; @@ -30,7 +31,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/similar_photos import 'package:immich_mobile/presentation/widgets/action_buttons/slideshow_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -244,7 +244,7 @@ enum ActionButtonType { menuItem: menuItem, ), ActionButtonType.likeActivity => LikeActivityActionButton(iconOnly: iconOnly, menuItem: menuItem), - ActionButtonType.unstack => UnStackActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), + ActionButtonType.unstack => ActionMenuItem(action: StackAction(source: context.source)), ActionButtonType.openInBrowser => OpenInBrowserActionButton( remoteId: context.asset.remoteId!, origin: context.timelineOrigin, diff --git a/mobile/test/unit/presentation/actions/stack_action_test.dart b/mobile/test/unit/presentation/actions/stack_action_test.dart new file mode 100644 index 0000000000..fdaf83886c --- /dev/null +++ b/mobile/test/unit/presentation/actions/stack_action_test.dart @@ -0,0 +1,94 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/stack.action.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../service.mocks.dart'; +import '../../factories/remote_asset_factory.dart'; +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + late MockAssetService assetService; + + setUp(() async { + context = await PresentationContext.create(); + assetService = context.service.asset.service; + }); + + tearDown(() { + context.dispose(); + }); + + RemoteAsset owned({String? stackId}) => RemoteAssetFactory.create(ownerId: context.currentUser.id, stackId: stackId); + + Future pumpStack(WidgetTester tester, Set selection) => + tester.pumpTestAction(context, const StackAction(source: .timeline), overrides: context.selected(selection)); + + group('StackAction', () { + testWidgets('stacks the eligible owned assets', (tester) async { + final first = owned(); + final second = owned(); + + await pumpStack(tester, {first, second}); + + verify(() => assetService.stack(context.currentUser.id, [first.id, second.id])).called(1); + }); + + testWidgets('unstacks the eligible owned assets', (tester) async { + final asset = owned(stackId: 'stack'); + + await pumpStack(tester, {asset}); + + verify(() => assetService.unstack(['stack'])).called(1); + }); + + testWidgets('prioritizes stack when mixed state', (tester) async { + final first = owned(); + final second = owned(stackId: 'stack'); + + await pumpStack(tester, {first, second}); + + verify(() => assetService.stack(context.currentUser.id, [first.id, second.id])).called(1); + }); + + testWidgets('ignores assets owned by someone else', (tester) async { + final mine = owned(); + final other = owned(); + final theirs = RemoteAssetFactory.create(); + + await pumpStack(tester, {mine, other, theirs}); + + verify(() => assetService.stack(context.currentUser.id, [mine.id, other.id])).called(1); + }); + + testWidgets('clears the selection once the stack succeeds', (tester) async { + await pumpStack(tester, {owned(), owned()}); + await tester.pumpAndSettle(); + + expect(find.byType(ImmichIconButton), findsNothing, reason: 'an empty selection hides the action'); + }); + + testWidgets('is hidden when a unstacked asset has nothing to stack onto', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: StackAction(source: .timeline)), + overrides: context.selected({owned()}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('is hidden when none of the selected assets are owned', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: StackAction(source: .timeline)), + overrides: context.selected({RemoteAssetFactory.create(), RemoteAssetFactory.create()}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); +} From e6b5b0deb6a4fbcaac47e1203dfab142e38c7e99 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Sat, 1 Aug 2026 08:06:40 +0530 Subject: [PATCH 33/81] feat: archive action (#29362) refactor: mobile archive action Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../presentation/actions/archive.action.dart | 66 ++++++++++ .../add_action_button.widget.dart | 36 ++---- .../archive_action_button.widget.dart | 60 --------- .../unarchive_action_button.widget.dart | 62 --------- .../archive_bottom_sheet.widget.dart | 4 +- .../favorite_bottom_sheet.widget.dart | 4 +- .../general_bottom_sheet.widget.dart | 4 +- .../remote_album_bottom_sheet.widget.dart | 4 +- .../infrastructure/action.provider.dart | 22 ---- mobile/lib/services/action.service.dart | 10 -- mobile/lib/utils/action_button.utils.dart | 11 +- .../actions/archive_action_test.dart | 118 ++++++++++++++++++ .../actions/restore_action_test.dart | 4 +- 13 files changed, 204 insertions(+), 201 deletions(-) create mode 100644 mobile/lib/presentation/actions/archive.action.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart create mode 100644 mobile/test/unit/presentation/actions/archive_action_test.dart diff --git a/mobile/lib/presentation/actions/archive.action.dart b/mobile/lib/presentation/actions/archive.action.dart new file mode 100644 index 0000000000..f04611c4c0 --- /dev/null +++ b/mobile/lib/presentation/actions/archive.action.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/providers/routes.provider.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; + +typedef _State = ({bool shouldArchive, List assetIds}); + +final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, source) { + if (ref.watch(inLockedViewProvider)) { + return null; + } + + final assets = ref.watch(ownedAssetsActionProvider(source)); + final shouldArchive = assets.notVisibility(.archive).isNotEmpty; + final assetIds = assets + .visibility(shouldArchive ? .timeline : .archive) + .map((asset) => asset.id) + .toList(growable: false); + return assetIds.isEmpty ? null : (shouldArchive: shouldArchive, assetIds: assetIds); +}); + +class ArchiveAction extends AssetActionBuilder { + const ArchiveAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + final shouldArchive = ref.watch(_stateProvider(source).select((state) => state?.shouldArchive)); + if (shouldArchive == null) { + return null; + } + + return .new( + icon: shouldArchive ? Icons.archive_outlined : Icons.unarchive_outlined, + label: shouldArchive ? context.t.archive : context.t.unarchive, + onAction: () => _archive(context, ref), + ); + } + + Future _archive(BuildContext context, WidgetRef ref) async { + final state = ref.read(_stateProvider(source)); + if (state == null) { + return; + } + + final (:shouldArchive, :assetIds) = state; + final message = shouldArchive + ? context.t.archive_action_prompt(count: assetIds.length) + : context.t.unarchive_action_prompt(count: assetIds.length); + final assetService = ref.read(assetServiceProvider); + final toastService = ref.read(toastServiceProvider); + final clearSelection = ref.read(clearSelectionProvider(source)); + + try { + await assetService.update(assetIds, visibility: .some(shouldArchive ? .archive : .timeline)); + toastService.success(message); + clearSelection(); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to update the archive status for assets"); + } + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart index 533bc72971..ea211797cf 100644 --- a/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart @@ -8,20 +8,20 @@ import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; -import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; +import 'package:immich_ui/immich_ui.dart'; -enum AddToMenuItem { album, archive, unarchive, lockedFolder } +enum AddToMenuItem { album, lockedFolder } class AddActionButton extends ConsumerStatefulWidget { const AddActionButton({super.key, this.originalTheme}); @@ -37,10 +37,6 @@ class _AddActionButtonState extends ConsumerState { switch (selected) { case AddToMenuItem.album: _openAlbumSelector(); - case AddToMenuItem.archive: - unawaited(performArchiveAction(context, ref, source: ActionSource.viewer)); - case AddToMenuItem.unarchive: - unawaited(performUnArchiveAction(context, ref, source: ActionSource.viewer)); case AddToMenuItem.lockedFolder: unawaited(performMoveToLockFolderAction(context, ref, source: ActionSource.viewer)); } @@ -54,11 +50,6 @@ class _AddActionButtonState extends ConsumerState { final user = ref.read(currentUserProvider); final isOwner = asset is RemoteAsset && asset.ownerId == user?.id; - final isInLockedView = ref.watch(inLockedViewProvider); - final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive; - final hasRemote = asset is RemoteAsset; - final showArchive = isOwner && !isInLockedView && hasRemote && !isArchived; - final showUnarchive = isOwner && !isInLockedView && hasRemote && isArchived; return [ Padding( @@ -78,20 +69,7 @@ class _AddActionButtonState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Text("move_to".tr(), style: context.textTheme.labelMedium), ), - if (showArchive) - BaseActionButton( - iconData: Icons.archive_outlined, - label: "archive".tr(), - menuItem: true, - onPressed: () => _handleMenuSelection(AddToMenuItem.archive), - ), - if (showUnarchive) - BaseActionButton( - iconData: Icons.unarchive_outlined, - label: "unarchive".tr(), - menuItem: true, - onPressed: () => _handleMenuSelection(AddToMenuItem.unarchive), - ), + const ActionMenuItem(action: ArchiveAction(source: .viewer)), BaseActionButton( iconData: Icons.lock_outline, label: "locked_folder".tr(), @@ -190,7 +168,7 @@ class _AddActionButtonState extends ConsumerState { final themeData = widget.originalTheme ?? context.themeData; - return MenuAnchor( + return ImmichMenu( consumeOutsideTap: true, style: MenuStyle( backgroundColor: WidgetStatePropertyAll(themeData.scaffoldBackgroundColor), @@ -201,7 +179,7 @@ class _AddActionButtonState extends ConsumerState { ), padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 6)), ), - menuChildren: widget.originalTheme != null + children: widget.originalTheme != null ? [ Theme( data: widget.originalTheme!, diff --git a/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart deleted file mode 100644 index 3322dd3a85..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/archive_action_button.widget.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; -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/events.model.dart'; -import 'package:immich_mobile/domain/utils/event_stream.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -// used to allow performing archive action from different sources (without duplicating code) -Future performArchiveAction(BuildContext context, WidgetRef ref, {required ActionSource source}) async { - if (!context.mounted) { - return; - } - - if (source == ActionSource.viewer) { - EventStream.shared.emit(const ViewerReloadAssetEvent()); - } - - final result = await ref.read(actionProvider.notifier).archive(source); - ref.read(multiSelectProvider.notifier).reset(); - - if (!context.mounted) { - return; - } - - final successMessage = 'archive_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); -} - -class ArchiveActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const ArchiveActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - Future _onTap(BuildContext context, WidgetRef ref) async { - await performArchiveAction(context, ref, source: source); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.archive_outlined, - label: "to_archive".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart deleted file mode 100644 index 552608f83f..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart +++ /dev/null @@ -1,62 +0,0 @@ -// dart -// File: `lib/presentation/widgets/action_buttons/unarchive_action_button.widget.dart` -import 'package:flutter/material.dart'; -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/events.model.dart'; -import 'package:immich_mobile/domain/utils/event_stream.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -// used to allow performing unarchive action from different sources (without duplicating code) -Future performUnArchiveAction(BuildContext context, WidgetRef ref, {required ActionSource source}) async { - if (!context.mounted) { - return; - } - - if (source == ActionSource.viewer) { - EventStream.shared.emit(const ViewerReloadAssetEvent()); - } - - final result = await ref.read(actionProvider.notifier).unArchive(source); - ref.read(multiSelectProvider.notifier).reset(); - - if (!context.mounted) { - return; - } - - final successMessage = 'unarchive_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); -} - -class UnArchiveActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const UnArchiveActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - Future _onTap(BuildContext context, WidgetRef ref) async { - await performUnArchiveAction(context, ref, source: source); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.unarchive_outlined, - label: "unarchive".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart index 7c3f72756c..509853a36e 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart @@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; @@ -15,7 +16,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_f import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; @@ -83,7 +83,7 @@ class _ArchiveBottomSheetState extends ConsumerState { const ShareActionButton(source: ActionSource.timeline), if (multiselect.hasRemote) ...[ const ShareLinkActionButton(source: ActionSource.timeline), - const UnArchiveActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: ArchiveAction(source: .timeline)), const ActionColumnButton(action: FavoriteAction(source: .timeline)), if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline), isTrashEnable diff --git a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart index 334d3a4309..fb92a084da 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart @@ -5,9 +5,9 @@ import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; @@ -84,7 +84,7 @@ class FavoriteBottomSheet extends ConsumerWidget { if (multiselect.hasRemote) ...[ const ShareLinkActionButton(source: ActionSource.timeline), const ActionColumnButton(action: FavoriteAction(source: .timeline)), - const ArchiveActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: ArchiveAction(source: .timeline)), if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline), isTrashEnable ? const TrashActionButton(source: ActionSource.timeline) diff --git a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart index a35223137a..6ffd23bfb1 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart @@ -4,9 +4,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; @@ -96,7 +96,7 @@ class _GeneralBottomSheetState extends ConsumerState { ? const TrashActionButton(source: ActionSource.timeline) : const DeletePermanentActionButton(source: ActionSource.timeline), const ActionColumnButton(action: FavoriteAction(source: .timeline)), - const ArchiveActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: ArchiveAction(source: .timeline)), if (tagsEnabled) const BulkTagAssetsActionButton(source: ActionSource.timeline), const EditDateTimeActionButton(source: ActionSource.timeline), const EditLocationActionButton(source: ActionSource.timeline), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart index 608a864101..7ffc4f41aa 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart @@ -4,9 +4,9 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; @@ -95,7 +95,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState const ShareLinkActionButton(source: ActionSource.timeline), if (ownsAlbum) ...[ - const ArchiveActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: ArchiveAction(source: .timeline)), const ActionColumnButton(action: FavoriteAction(source: .timeline)), ], const DownloadActionButton(source: ActionSource.timeline), diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index 11eda6d3de..30d815f1e0 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -118,28 +118,6 @@ class ActionNotifier extends Notifier { } } - Future archive(ActionSource source) async { - final ids = _getOwnedRemoteIdsForSource(source); - try { - await _service.archive(ids); - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to archive assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - - Future unArchive(ActionSource source) async { - final ids = _getOwnedRemoteIdsForSource(source); - try { - await _service.unArchive(ids); - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to unarchive assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - Future moveToLockFolder(ActionSource source) async { final ids = _getOwnedRemoteIdsForSource(source); final localIds = _getLocalIdsForSource(source, ignoreLocalOnly: true); diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index ad4f3098b8..12e3bd6b93 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -68,16 +68,6 @@ class ActionService { unawaited(context.pushRoute(SharedLinkEditRoute(assetsList: remoteIds))); } - Future archive(List remoteIds) async { - await _assetApiRepository.updateVisibility(remoteIds, .archive); - await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.archive); - } - - Future unArchive(List remoteIds) async { - await _assetApiRepository.updateVisibility(remoteIds, .timeline); - await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.timeline); - } - Future moveToLockFolder(List remoteIds, List localIds) async { await _assetApiRepository.updateVisibility(remoteIds, .locked); await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.locked); diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 8b51a5ea6a..3e70205702 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -8,10 +8,10 @@ import 'package:immich_mobile/domain/models/events.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; import 'package:immich_mobile/presentation/actions/restore.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; @@ -30,7 +30,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act import 'package:immich_mobile/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/slideshow_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -200,12 +199,8 @@ enum ActionButtonType { menuItem: menuItem, ), ActionButtonType.slideshow => SlideshowActionButton(iconOnly: iconOnly, menuItem: menuItem), - ActionButtonType.archive => ArchiveActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), - ActionButtonType.unarchive => UnArchiveActionButton( - source: context.source, - iconOnly: iconOnly, - menuItem: menuItem, - ), + ActionButtonType.archive || + ActionButtonType.unarchive => ActionMenuItem(action: ArchiveAction(source: context.source)), ActionButtonType.download => DownloadActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), ActionButtonType.trash => TrashActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), ActionButtonType.restoreTrash => ActionMenuItem(action: RestoreAction(source: context.source)), diff --git a/mobile/test/unit/presentation/actions/archive_action_test.dart b/mobile/test/unit/presentation/actions/archive_action_test.dart new file mode 100644 index 0000000000..82f015903f --- /dev/null +++ b/mobile/test/unit/presentation/actions/archive_action_test.dart @@ -0,0 +1,118 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/archive.action.dart'; +import 'package:immich_mobile/providers/routes.provider.dart'; +import 'package:immich_mobile/utils/option.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../service.mocks.dart'; +import '../../factories/remote_asset_factory.dart'; +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + late MockAssetService assetService; + + setUp(() async { + context = await PresentationContext.create(); + assetService = context.service.asset.service; + }); + + tearDown(() { + context.dispose(); + }); + + RemoteAsset owned({AssetVisibility visibility = .timeline}) => + RemoteAssetFactory.create(ownerId: context.currentUser.id, visibility: visibility); + + Future pumpArchive(WidgetTester tester, Set selection) => + tester.pumpTestAction(context, const ArchiveAction(source: .timeline), overrides: context.selected(selection)); + + group('ArchiveAction', () { + testWidgets('archives the eligible owned assets', (tester) async { + final asset = owned(); + + await pumpArchive(tester, {asset}); + + verify(() => assetService.update([asset.id], visibility: const Option.some(AssetVisibility.archive))).called(1); + }); + + testWidgets('unarchive the eligible owned assets', (tester) async { + final asset = owned(visibility: .archive); + + await pumpArchive(tester, {asset}); + + verify(() => assetService.update([asset.id], visibility: const .some(.timeline))).called(1); + }); + + testWidgets('prioritizes archive when mixed state', (tester) async { + final onTimeline = owned(); + final archived = owned(visibility: .archive); + + await pumpArchive(tester, {onTimeline, archived}); + + verify(() => assetService.update([onTimeline.id], visibility: const .some(.archive))).called(1); + verifyNever(() => assetService.update(any(), visibility: const .some(.timeline))); + }); + + testWidgets('ignores assets owned by someone else', (tester) async { + final mine = owned(); + final theirs = RemoteAssetFactory.create(); + + await pumpArchive(tester, {mine, theirs}); + + verify(() => assetService.update([mine.id], visibility: const .some(.archive))).called(1); + }); + + testWidgets('skips owned assets already in the target state', (tester) async { + final stale = owned(); + final alreadyArchived = owned(visibility: .archive); + + await pumpArchive(tester, {stale, alreadyArchived}); + + verify(() => assetService.update([stale.id], visibility: const .some(.archive))).called(1); + }); + + testWidgets('clears the selection once the update succeeds', (tester) async { + await pumpArchive(tester, {owned()}); + await tester.pumpAndSettle(); + + expect(find.byType(ImmichIconButton), findsNothing, reason: 'an empty selection hides the action'); + }); + + testWidgets('is hidden for locked assets, which belong to neither direction', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: ArchiveAction(source: .timeline)), + overrides: context.selected({owned(visibility: .locked)}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('is hidden inside the locked folder view', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: ArchiveAction(source: .timeline)), + overrides: [ + ...context.selected({owned()}), + inLockedViewProvider.overrideWithValue(true), + ], + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('is hidden when none of the selected assets are owned', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: ArchiveAction(source: .timeline)), + overrides: context.selected({RemoteAssetFactory.create()}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); +} diff --git a/mobile/test/unit/presentation/actions/restore_action_test.dart b/mobile/test/unit/presentation/actions/restore_action_test.dart index d7992850e5..4597e7ccb0 100644 --- a/mobile/test/unit/presentation/actions/restore_action_test.dart +++ b/mobile/test/unit/presentation/actions/restore_action_test.dart @@ -23,7 +23,7 @@ void main() { }); RemoteAsset owned({bool trashed = true}) => - RemoteAssetFactory.create(ownerId: context.currentUser.id, deletedAt: trashed ? DateTime(2020) : null); + RemoteAssetFactory.create(ownerId: context.currentUser.id, deletedAt: trashed ? .new(2020) : null); Future pumpRestore(WidgetTester tester, Set selection) => tester.pumpTestAction(context, const RestoreAction(source: .timeline), overrides: context.selected(selection)); @@ -39,7 +39,7 @@ void main() { testWidgets('ignores assets owned by someone else', (tester) async { final mine = owned(); - final theirs = RemoteAssetFactory.create(deletedAt: DateTime(2020)); + final theirs = RemoteAssetFactory.create(deletedAt: .new(2020)); await pumpRestore(tester, {mine, theirs}); From 47d34e3b4b17e34f4e4e5c0d2d29f9e15d2f16a9 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Sat, 1 Aug 2026 08:06:41 +0530 Subject: [PATCH 34/81] refactor: mobile lock action (#29767) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- mobile/lib/domain/services/asset.service.dart | 28 ++++- .../lib/presentation/actions/lock.action.dart | 71 +++++++++++ .../add_action_button.widget.dart | 13 +- ...e_to_lock_folder_action_button.widget.dart | 63 ---------- ...from_lock_folder_action_button.widget.dart | 57 --------- .../asset_viewer/bottom_bar.widget.dart | 13 +- .../archive_bottom_sheet.widget.dart | 4 +- .../favorite_bottom_sheet.widget.dart | 4 +- .../general_bottom_sheet.widget.dart | 4 +- .../locked_folder_bottom_sheet.widget.dart | 5 +- .../remote_album_bottom_sheet.widget.dart | 4 +- .../infrastructure/action.provider.dart | 23 ---- .../infrastructure/asset.provider.dart | 3 + mobile/lib/services/action.service.dart | 15 --- mobile/lib/utils/action_button.utils.dart | 15 +-- mobile/test/unit/mocks.dart | 4 + .../actions/lock_action_test.dart | 114 ++++++++++++++++++ .../unit/services/asset_service_test.dart | 2 + 18 files changed, 249 insertions(+), 193 deletions(-) create mode 100644 mobile/lib/presentation/actions/lock.action.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart create mode 100644 mobile/test/unit/presentation/actions/lock_action_test.dart diff --git a/mobile/lib/domain/services/asset.service.dart b/mobile/lib/domain/services/asset.service.dart index 9cbdf1bdfe..59f41f1e51 100644 --- a/mobile/lib/domain/services/asset.service.dart +++ b/mobile/lib/domain/services/asset.service.dart @@ -2,10 +2,15 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_exif.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/utils/option.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -14,12 +19,16 @@ class AssetService { final RemoteExifRepository _exifRepository; final DriftLocalAssetRepository _localRepository; final AssetApiRepository _apiRepository; + final AssetMediaRepository _mediaRepository; + final DriftTrashedLocalAssetRepository _trashedLocalRepository; const AssetService({ required this._remoteRepository, required this._exifRepository, required this._localRepository, required this._apiRepository, + required this._mediaRepository, + required this._trashedLocalRepository, }); Future getAsset(BaseAsset asset) { @@ -167,7 +176,24 @@ class AssetService { } } - // TODO(shenlong): remove after action migration + Future deleteLocal(List localIds) async { + if (localIds.isEmpty) { + return 0; + } + + final deletedIds = await _mediaRepository.deleteAll(localIds); + if (deletedIds.isEmpty) { + return 0; + } + + if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) { + await _trashedLocalRepository.applyTrashedAssets(deletedIds); + } else { + await _localRepository.delete(deletedIds); + } + return deletedIds.length; + } + Future getLocalAsset(String id) { return _localRepository.get(id); } diff --git a/mobile/lib/presentation/actions/lock.action.dart b/mobile/lib/presentation/actions/lock.action.dart new file mode 100644 index 0000000000..b7fd01ad18 --- /dev/null +++ b/mobile/lib/presentation/actions/lock.action.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; + +typedef _State = ({bool shouldLock, List assetIds, List localIds}); + +final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, source) { + final assets = ref.watch(ownedAssetsActionProvider(source)); + if (assets.isEmpty) { + return null; + } + + final shouldLock = assets.locked(isLocked: false).isNotEmpty; + final targets = assets.locked(isLocked: !shouldLock); + return ( + shouldLock: shouldLock, + assetIds: targets.map((asset) => asset.id).toList(growable: false), + // 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 [], + ); +}); + +class LockAction extends AssetActionBuilder { + const LockAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + final shouldLock = ref.watch(_stateProvider(source).select((state) => state?.shouldLock)); + if (shouldLock == null) { + return null; + } + + return .new( + icon: shouldLock ? Icons.lock_rounded : Icons.lock_open_rounded, + label: shouldLock ? context.t.move_to_locked_folder : context.t.remove_from_locked_folder, + onAction: () => _lock(context, ref), + ); + } + + Future _lock(BuildContext context, WidgetRef ref) async { + final state = ref.read(_stateProvider(source)); + if (state == null) { + return; + } + + final (:shouldLock, :assetIds, :localIds) = state; + final message = shouldLock + ? context.t.move_to_lock_folder_action_prompt(count: assetIds.length) + : context.t.remove_from_lock_folder_action_prompt(count: assetIds.length); + final assetService = ref.read(assetServiceProvider); + final toastService = ref.read(toastServiceProvider); + final clearSelection = ref.read(clearSelectionProvider(source)); + + try { + await assetService.update(assetIds, visibility: .some(shouldLock ? .locked : .timeline)); + if (localIds.isNotEmpty) { + // A locked asset still sits in the device gallery, so offer to remove the local copy. + await assetService.deleteLocal(localIds); + } + toastService.success(message); + clearSelection(); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to update the locked folder for assets"); + } + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart index ea211797cf..cfb5887c7c 100644 --- a/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/add_action_button.widget.dart @@ -10,8 +10,8 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; +import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; @@ -21,7 +21,7 @@ import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_ui/immich_ui.dart'; -enum AddToMenuItem { album, lockedFolder } +enum AddToMenuItem { album } class AddActionButton extends ConsumerStatefulWidget { const AddActionButton({super.key, this.originalTheme}); @@ -37,8 +37,6 @@ class _AddActionButtonState extends ConsumerState { switch (selected) { case AddToMenuItem.album: _openAlbumSelector(); - case AddToMenuItem.lockedFolder: - unawaited(performMoveToLockFolderAction(context, ref, source: ActionSource.viewer)); } } @@ -70,12 +68,7 @@ class _AddActionButtonState extends ConsumerState { child: Text("move_to".tr(), style: context.textTheme.labelMedium), ), const ActionMenuItem(action: ArchiveAction(source: .viewer)), - BaseActionButton( - iconData: Icons.lock_outline, - label: "locked_folder".tr(), - menuItem: true, - onPressed: () => _handleMenuSelection(AddToMenuItem.lockedFolder), - ), + const ActionMenuItem(action: LockAction(source: .viewer)), ], ]; } diff --git a/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart deleted file mode 100644 index 6d5f5b387a..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:flutter/material.dart'; -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/events.model.dart'; -import 'package:immich_mobile/domain/utils/event_stream.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -// Reusable helper: move to locked folder from any source (e.g called from menu) -Future performMoveToLockFolderAction(BuildContext context, WidgetRef ref, {required ActionSource source}) async { - if (!context.mounted) { - return; - } - - if (source == ActionSource.viewer) { - EventStream.shared.emit(const ViewerReloadAssetEvent()); - } - - final result = await ref.read(actionProvider.notifier).moveToLockFolder(source); - ref.read(multiSelectProvider.notifier).reset(); - if (!context.mounted) { - return; - } - - final successMessage = 'move_to_lock_folder_action_prompt'.t( - context: context, - args: {'count': result.count.toString()}, - ); - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); -} - -class MoveToLockFolderActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const MoveToLockFolderActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - Future _onTap(BuildContext context, WidgetRef ref) async { - await performMoveToLockFolderAction(context, ref, source: source); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - maxWidth: 115.0, - iconData: Icons.lock_outline_rounded, - label: "move_to_locked_folder".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart deleted file mode 100644 index ea0d9f384f..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class RemoveFromLockFolderActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const RemoveFromLockFolderActionButton({ - super.key, - required this.source, - this.iconOnly = false, - this.menuItem = false, - }); - - Future _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final result = await ref.read(actionProvider.notifier).removeFromLockFolder(source); - ref.read(multiSelectProvider.notifier).reset(); - if (!context.mounted) { - return; - } - - final successMessage = 'remove_from_lock_folder_action_prompt'.t( - context: context, - args: {'count': result.count.toString()}, - ); - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - maxWidth: 100.0, - iconData: Icons.lock_open_rounded, - label: "remove_from_locked_folder".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart index 14aa768626..2819d2502d 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -4,7 +4,7 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; import 'package:immich_mobile/presentation/actions/restore.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; @@ -22,6 +22,15 @@ import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/utils/semver.dart'; import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart'; +import 'package:immich_ui/immich_ui.dart'; + +// Resolved here rather than as an ActionColumnButton so a hidden restore +// takes no slot in the spaceEvenly row below. +List _actionColumnButtons(BuildContext context, WidgetRef ref, List actions) => actions + .map((a) => a.create(context, ref)) + .nonNulls + .map((item) => ImmichColumnButton(icon: item.icon, label: item.label, onPressed: item.onAction)) + .toList(growable: false); class ViewerBottomBar extends ConsumerWidget { const ViewerBottomBar({super.key}); @@ -44,7 +53,7 @@ class ViewerBottomBar extends ConsumerWidget { final originalTheme = context.themeData; final actions = [ - const ActionColumnButton(action: RestoreAction(source: .viewer)), + ..._actionColumnButtons(context, ref, const [RestoreAction(source: .viewer)]), const ShareActionButton(source: .viewer), if (!isInLockedView) ...[ diff --git a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart index 509853a36e..5b041452c0 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart @@ -6,13 +6,13 @@ import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; +import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; @@ -91,7 +91,7 @@ class _ArchiveBottomSheetState extends ConsumerState { : const DeletePermanentActionButton(source: ActionSource.timeline), const EditDateTimeActionButton(source: ActionSource.timeline), const EditLocationActionButton(source: ActionSource.timeline), - const MoveToLockFolderActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: LockAction(source: .timeline)), const ActionColumnButton(action: StackAction(source: .timeline)), ], if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart index fb92a084da..7ae0fb6055 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart @@ -7,13 +7,13 @@ import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; +import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; @@ -91,7 +91,7 @@ class FavoriteBottomSheet extends ConsumerWidget { : const DeletePermanentActionButton(source: ActionSource.timeline), const EditDateTimeActionButton(source: ActionSource.timeline), const EditLocationActionButton(source: ActionSource.timeline), - const MoveToLockFolderActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: LockAction(source: .timeline)), const ActionColumnButton(action: StackAction(source: .timeline)), ], if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart index 6ffd23bfb1..40e75adf86 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart @@ -6,6 +6,7 @@ import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; +import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; @@ -14,7 +15,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permane import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; @@ -100,7 +100,7 @@ class _GeneralBottomSheetState extends ConsumerState { if (tagsEnabled) const BulkTagAssetsActionButton(source: ActionSource.timeline), const EditDateTimeActionButton(source: ActionSource.timeline), const EditLocationActionButton(source: ActionSource.timeline), - const MoveToLockFolderActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: LockAction(source: .timeline)), const ActionColumnButton(action: StackAction(source: .timeline)), if (multiselect.onlyLocal || multiselect.hasMerged) const DeleteActionButton(source: ActionSource.timeline), ], diff --git a/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart index a644e6a035..9b4fef1a13 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; @@ -20,7 +21,7 @@ class LockedFolderBottomSheet extends ConsumerWidget { ShareActionButton(source: ActionSource.timeline), DownloadActionButton(source: ActionSource.timeline), DeletePermanentActionButton(source: ActionSource.timeline), - RemoveFromLockFolderActionButton(source: ActionSource.timeline), + ActionColumnButton(action: LockAction(source: .timeline)), ], ); } diff --git a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart index 7ffc4f41aa..ac9b85caa1 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart @@ -6,13 +6,13 @@ import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; +import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; @@ -105,7 +105,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState : const DeletePermanentActionButton(source: ActionSource.timeline), const EditDateTimeActionButton(source: ActionSource.timeline), const EditLocationActionButton(source: ActionSource.timeline), - const MoveToLockFolderActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: LockAction(source: .timeline)), const ActionColumnButton(action: StackAction(source: .timeline)), ], ], diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index 30d815f1e0..20826ddd8e 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -118,29 +118,6 @@ class ActionNotifier extends Notifier { } } - Future moveToLockFolder(ActionSource source) async { - final ids = _getOwnedRemoteIdsForSource(source); - final localIds = _getLocalIdsForSource(source, ignoreLocalOnly: true); - try { - await _service.moveToLockFolder(ids, localIds); - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to move assets to lock folder', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - - Future removeFromLockFolder(ActionSource source) async { - final ids = _getOwnedRemoteIdsForSource(source); - try { - await _service.removeFromLockFolder(ids); - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to remove assets from lock folder', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - Future trash(ActionSource source) async { final ids = _getOwnedRemoteIdsForSource(source); diff --git a/mobile/lib/providers/infrastructure/asset.provider.dart b/mobile/lib/providers/infrastructure/asset.provider.dart index fed55208e4..8ca0ca0ec9 100644 --- a/mobile/lib/providers/infrastructure/asset.provider.dart +++ b/mobile/lib/providers/infrastructure/asset.provider.dart @@ -7,6 +7,7 @@ import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.re import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/repositories/asset_media.repository.dart'; final localAssetRepository = Provider( (ref) => DriftLocalAssetRepository(ref.watch(driftProvider)), @@ -28,6 +29,8 @@ final assetServiceProvider = Provider( exifRepository: ref.watch(remoteExifRepositoryProvider), localRepository: ref.watch(localAssetRepository), apiRepository: ref.watch(assetApiRepositoryProvider), + mediaRepository: ref.watch(assetMediaRepositoryProvider), + trashedLocalRepository: ref.watch(trashedLocalAssetRepository), ), ); diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index 12e3bd6b93..00b608c8a6 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -68,21 +68,6 @@ class ActionService { unawaited(context.pushRoute(SharedLinkEditRoute(assetsList: remoteIds))); } - Future moveToLockFolder(List remoteIds, List localIds) async { - await _assetApiRepository.updateVisibility(remoteIds, .locked); - await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.locked); - - // Ask user if they want to delete local copies - if (localIds.isNotEmpty) { - await _deleteLocalAssets(localIds); - } - } - - Future removeFromLockFolder(List remoteIds) async { - await _assetApiRepository.updateVisibility(remoteIds, .timeline); - await _remoteAssetRepository.updateVisibility(remoteIds, AssetVisibility.timeline); - } - Future trash(List remoteIds) async { await _assetApiRepository.delete(remoteIds, false); await _remoteAssetRepository.trash(remoteIds); diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 3e70205702..2924907423 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -10,6 +10,7 @@ import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; +import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/actions/restore.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; @@ -19,10 +20,8 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_a import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; @@ -210,16 +209,8 @@ enum ActionButtonType { menuItem: menuItem, ), ActionButtonType.delete => DeleteActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), - ActionButtonType.moveToLockFolder => MoveToLockFolderActionButton( - source: context.source, - iconOnly: iconOnly, - menuItem: menuItem, - ), - ActionButtonType.removeFromLockFolder => RemoveFromLockFolderActionButton( - source: context.source, - iconOnly: iconOnly, - menuItem: menuItem, - ), + ActionButtonType.moveToLockFolder || + ActionButtonType.removeFromLockFolder => ActionMenuItem(action: LockAction(source: context.source)), ActionButtonType.deleteLocal => DeleteLocalActionButton( source: context.source, iconOnly: iconOnly, diff --git a/mobile/test/unit/mocks.dart b/mobile/test/unit/mocks.dart index 7dd15eb9a3..31dfb6f862 100644 --- a/mobile/test/unit/mocks.dart +++ b/mobile/test/unit/mocks.dart @@ -167,6 +167,7 @@ class ServiceMocks { when(asset.trash).thenAnswer((_) async {}); when(asset.delete).thenAnswer((_) async {}); when(asset.applyEdits).thenAnswer((_) async {}); + when(asset.deleteLocal).thenAnswer((_) async => 0); } void _stubRemoteAlbumService() { @@ -342,6 +343,9 @@ extension type const AssetServiceStub(MockAssetService service) implements Stub< Future Function() get applyEdits => () => service.applyEdits(any(), any()); + + Future Function() get deleteLocal => + () => service.deleteLocal(any()); } extension type const RemoteAlbumServiceStub(MockRemoteAlbumService service) implements Stub { diff --git a/mobile/test/unit/presentation/actions/lock_action_test.dart b/mobile/test/unit/presentation/actions/lock_action_test.dart new file mode 100644 index 0000000000..f5ffbd73d4 --- /dev/null +++ b/mobile/test/unit/presentation/actions/lock_action_test.dart @@ -0,0 +1,114 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/lock.action.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../service.mocks.dart'; +import '../../factories/remote_asset_factory.dart'; +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + late MockAssetService assetService; + + setUp(() async { + context = await PresentationContext.create(); + assetService = context.service.asset.service; + }); + + tearDown(() { + context.dispose(); + }); + + RemoteAsset owned({AssetVisibility visibility = .timeline}) => + RemoteAssetFactory.create(ownerId: context.currentUser.id, visibility: visibility); + + Future pumpLock(WidgetTester tester, Set selection) => + tester.pumpTestAction(context, const LockAction(source: .timeline), overrides: context.selected(selection)); + + group('LockAction', () { + testWidgets('locks the eligible owned assets', (tester) async { + final asset = owned(); + + await pumpLock(tester, {asset}); + + verify(() => assetService.update([asset.id], visibility: const .some(.locked))).called(1); + }); + + testWidgets('unlocks the eligible owned assets', (tester) async { + final asset = owned(visibility: .locked); + + await pumpLock(tester, {asset}); + + verify(() => assetService.update([asset.id], visibility: const .some(.timeline))).called(1); + }); + + testWidgets('prioritizes lock when mixed state', (tester) async { + final unlocked = owned(); + final locked = owned(visibility: .locked); + + await pumpLock(tester, {unlocked, locked}); + + verify(() => assetService.update([unlocked.id], visibility: const .some(.locked))).called(1); + verifyNever(() => assetService.update(any(), visibility: const .some(.timeline))); + }); + + testWidgets('ignores assets owned by someone else', (tester) async { + final mine = owned(); + final theirs = RemoteAssetFactory.create(); + + await pumpLock(tester, {mine, theirs}); + + verify(() => assetService.update([mine.id], visibility: const .some(.locked))).called(1); + }); + + testWidgets('locks only the owned assets not already locked', (tester) async { + final stale = owned(); + final alreadyLocked = owned(visibility: .locked); + + await pumpLock(tester, {stale, alreadyLocked}); + + verify(() => assetService.update([stale.id], visibility: const .some(.locked))).called(1); + }); + + testWidgets('removes the local copies of the assets it locks', (tester) async { + final merged = RemoteAssetFactory.create(ownerId: context.currentUser.id, localId: 'local-1'); + final remoteOnly = owned(); + + await pumpLock(tester, {merged, remoteOnly}); + + verify(() => assetService.deleteLocal(['local-1'])).called(1); + }); + + testWidgets('leaves the local copies alone when unlocking', (tester) async { + final merged = RemoteAssetFactory.create( + ownerId: context.currentUser.id, + localId: 'local-1', + visibility: .locked, + ); + + await pumpLock(tester, {merged}); + + verifyNever(() => assetService.deleteLocal(any())); + }); + + testWidgets('clears the selection once the update succeeds', (tester) async { + await pumpLock(tester, {owned()}); + await tester.pumpAndSettle(); + + expect(find.byType(ImmichIconButton), findsNothing, reason: 'an empty selection hides the action'); + }); + + testWidgets('is hidden when none of the selected assets are owned', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: LockAction(source: .timeline)), + overrides: context.selected({RemoteAssetFactory.create()}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); +} diff --git a/mobile/test/unit/services/asset_service_test.dart b/mobile/test/unit/services/asset_service_test.dart index 5e473d8f66..a09465701b 100644 --- a/mobile/test/unit/services/asset_service_test.dart +++ b/mobile/test/unit/services/asset_service_test.dart @@ -24,6 +24,8 @@ void main() { exifRepository: exifRepository, localRepository: MockDriftLocalAssetRepository(), apiRepository: apiRepository, + mediaRepository: mocks.assetMedia.api, + trashedLocalRepository: mocks.trashedAsset, ); }); From 3f3bc44258c9b39ddaab59343a9430a15f227cd0 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Sat, 1 Aug 2026 08:06:41 +0530 Subject: [PATCH 35/81] refactor: mobile delete action (#29771) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../presentation/actions/delete.action.dart | 229 ++++++++++++++++ .../delete_action_button.widget.dart | 100 ------- .../delete_local_action_button.widget.dart | 69 ----- ...delete_permanent_action_button.widget.dart | 80 ------ .../delete_trash_action_button.widget.dart | 67 ----- .../trash_action_button.widget.dart | 58 ---- .../asset_viewer/bottom_bar.widget.dart | 17 +- .../archive_bottom_sheet.widget.dart | 12 +- .../favorite_bottom_sheet.widget.dart | 12 +- .../general_bottom_sheet.widget.dart | 15 +- .../local_album_bottom_sheet.widget.dart | 6 +- .../locked_folder_bottom_sheet.widget.dart | 4 +- .../remote_album_bottom_sheet.widget.dart | 12 +- .../trash_bottom_sheet.widget.dart | 5 +- .../infrastructure/action.provider.dart | 85 ------ mobile/lib/services/action.service.dart | 51 ---- mobile/lib/utils/action_button.utils.dart | 41 +-- mobile/test/services/action.service_test.dart | 53 ---- .../actions/delete_action_test.dart | 257 ++++++++++++++++++ .../presentation/presentation_context.dart | 2 + .../action_button_utils_test.dart | 112 +------- 21 files changed, 517 insertions(+), 770 deletions(-) create mode 100644 mobile/lib/presentation/actions/delete.action.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart create mode 100644 mobile/test/unit/presentation/actions/delete_action_test.dart diff --git a/mobile/lib/presentation/actions/delete.action.dart b/mobile/lib/presentation/actions/delete.action.dart new file mode 100644 index 0000000000..0fe297bd03 --- /dev/null +++ b/mobile/lib/presentation/actions/delete.action.dart @@ -0,0 +1,229 @@ +import 'package:flutter/material.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/extensions/platform_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/services/cleanup.service.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; +import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; + +typedef _State = ({List localIds, List remoteIds, bool trash}); + +final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, source) { + final assets = ref.watch(assetsActionProvider(source)); + final authUserId = ref.watch(authUserProvider).id; + + final localIds = []; + final ownedRemote = []; + for (final asset in assets) { + if (asset.localId case final localId?) { + localIds.add(localId); + } + if (asset case final RemoteAsset remote when remote.ownerId == authUserId) { + ownedRemote.add(remote); + } + } + + if (localIds.isEmpty && ownedRemote.isEmpty) { + return null; + } + + final trashEnabled = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); + // Assets already in the trash or in the locked folder are deleted outright, irrespective of the server setting. + final trash = trashEnabled && !ownedRemote.every((asset) => asset.isTrashed || asset.isLocked); + + return (localIds: localIds, remoteIds: ownedRemote.map((asset) => asset.id).toList(growable: false), trash: trash); +}); + +class DeleteAction extends AssetActionBuilder { + const DeleteAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + final trash = ref.watch(_stateProvider(source).select((state) => state?.trash)); + if (trash == null) { + return null; + } + + return .new( + icon: Icons.delete_outline, + label: trash ? context.t.trash : context.t.delete, + onAction: () => _delete(context, ref), + ); + } + + Future _delete(BuildContext context, WidgetRef ref) async { + final state = ref.read(_stateProvider(source)); + if (state == null) { + return; + } + + final (:localIds, :remoteIds, :trash) = state; + final toastService = ref.read(toastServiceProvider); + final clearSelection = ref.read(clearSelectionProvider(source)); + + try { + final String? message; + if (remoteIds.isEmpty) { + message = await _removeLocalAssets(context, ref, localIds); + } else if (trash) { + message = await _moveToTrash(context, ref, remoteIds, localIds); + } else { + message = await _deletePermanently(context, ref, remoteIds, localIds); + } + + if (message == null) { + return; + } + + toastService.success(message); + clearSelection(); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to delete assets"); + } + } + + Future _removeLocalAssets(BuildContext context, WidgetRef ref, List localIds) async { + final count = await _cleanupLocalAssets(context, ref, localIds); + if (count <= 0 || !context.mounted) { + return null; + } + + return context.t.cleanup_deleted_assets(count: count); + } + + Future _moveToTrash( + BuildContext context, + WidgetRef ref, + List remoteIds, + List localIds, + ) async { + final assetService = ref.read(assetServiceProvider); + if (localIds.isNotEmpty) { + await _cleanupLocalAssets(context, ref, localIds); + if (!context.mounted) { + return null; + } + } + + final message = context.t.trash_action_prompt(count: remoteIds.length); + await assetService.trash(remoteIds); + return message; + } + + Future _deletePermanently( + BuildContext context, + WidgetRef ref, + List remoteIds, + List localIds, + ) async { + final assetService = ref.read(assetServiceProvider); + final confirmed = await showDialog( + context: context, + builder: (_) => + const ConfirmDialog(title: 'delete_dialog_title', content: 'delete_dialog_alert', ok: 'delete_permanently'), + ); + if (confirmed != true || !context.mounted) { + return null; + } + + final message = context.t.delete_permanently_action_prompt(count: remoteIds.length); + // Server first, so a failed request will not remove the local copy + await assetService.delete(remoteIds); + if (localIds.isNotEmpty && context.mounted) { + await _cleanupLocalAssets(context, ref, localIds, requestCustomPrompt: false); + } + + return message; + } +} + +final _cleanupStateProvider = Provider.family.autoDispose?, ActionSource>((ref, source) { + final assets = ref.watch(assetsActionProvider(source)); + final assetIds = assets.backedUp().map((asset) => asset.localId).nonNulls.toList(growable: false); + return assetIds.isEmpty ? null : assetIds; +}); + +class CleanupLocalAction extends AssetActionBuilder { + const CleanupLocalAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + final isVisible = ref.watch(_cleanupStateProvider(source).select((state) => state != null)); + if (!isVisible) { + return null; + } + + return .new( + icon: Icons.no_cell_outlined, + label: context.t.control_bottom_app_bar_delete_from_local, + onAction: () => _cleanup(context, ref), + ); + } + + Future _cleanup(BuildContext context, WidgetRef ref) async { + final assetIds = ref.read(_cleanupStateProvider(source)); + if (assetIds == null) { + return; + } + + final toastService = ref.read(toastServiceProvider); + final clearSelection = ref.read(clearSelectionProvider(source)); + + try { + final count = await _cleanupLocalAssets(context, ref, assetIds); + if (count <= 0 || !context.mounted) { + return; + } + + toastService.success(context.t.cleanup_deleted_assets(count: count)); + clearSelection(); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to remove the device copies"); + } + } +} + +/// Removes the device copies of [assetIds], returning how many were deleted. +/// +/// iOS and Android without MANAGE_MEDIA prompt the user +/// with MANAGE_MEDIA, we do it ourselves unless [requestCustomPrompt] is false. +Future _cleanupLocalAssets( + BuildContext context, + WidgetRef ref, + List assetIds, { + bool requestCustomPrompt = true, +}) async { + if (assetIds.isEmpty) { + return 0; + } + + final cleanupService = ref.read(cleanupServiceProvider); + final requiresPrompt = + requestCustomPrompt && + CurrentPlatform.isAndroid && + ref.read(storeServiceProvider).get(.manageLocalMediaAndroid, false); + + if (requiresPrompt) { + final confirmed = await showDialog( + context: context, + builder: (_) => ConfirmDialog( + title: context.t.move_to_device_trash, + content: context.t.free_up_space_description, + ok: context.t.ok, + ), + ); + if (confirmed != true) { + return 0; + } + } + + return cleanupService.deleteLocalAssets(assetIds); +} diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart deleted file mode 100644 index a6ed4c4246..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/delete_action_button.widget.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:flutter/material.dart'; -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/events.model.dart'; -import 'package:immich_mobile/domain/utils/event_stream.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_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 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -/// This delete action has the following behavior: -/// - Set the deletedAt information, put the asset in the trash in the server -/// which will be permanently deleted after the number of days configure by the admin -/// - Prompt to delete the asset locally -class DeleteActionButton extends ConsumerWidget { - final ActionSource source; - final bool showConfirmation; - final bool iconOnly; - final bool menuItem; - const DeleteActionButton({ - super.key, - required this.source, - this.showConfirmation = false, - this.iconOnly = false, - this.menuItem = false, - }); - - Future _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - if (showConfirmation) { - final confirm = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text('delete'.t(context: context)), - content: Text('delete_action_confirmation_message'.t(context: context)), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text('cancel'.t(context: context)), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: Text( - 'confirm'.t(context: context), - style: TextStyle(color: context.colorScheme.error), - ), - ), - ], - ), - ); - if (confirm != true) { - return; - } - } - - final currentAsset = ref.read(assetViewerProvider).currentAsset; - final stackIndex = ref.read(assetViewerProvider).stackIndex; - - final result = await ref.read(actionProvider.notifier).trashRemoteAndDeleteLocal(source); - ref.read(multiSelectProvider.notifier).reset(); - - if (source == ActionSource.viewer && result.success) { - final shouldRefreshStack = currentAsset is RemoteAsset && currentAsset.stackId != null; - EventStream.shared.emit( - shouldRefreshStack ? ViewerStackAssetDeletedEvent(stackIndex: stackIndex) : const ViewerReloadAssetEvent(), - ); - } - if (!context.mounted) { - return; - } - - final successMessage = 'delete_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - maxWidth: 110.0, - iconData: Icons.delete_sweep_outlined, - label: "delete".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart deleted file mode 100644 index 09969d8b8a..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/delete_local_action_button.widget.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:flutter/material.dart'; -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/events.model.dart'; -import 'package:immich_mobile/domain/utils/event_stream.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -/// This delete action has the following behavior: -/// - Prompt to delete the asset locally -class DeleteLocalActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const DeleteLocalActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - Future _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final result = await ref.read(actionProvider.notifier).deleteLocal(source, context); - if (result == null) { - return; - } - - ref.read(multiSelectProvider.notifier).reset(); - - if (source == ActionSource.viewer) { - EventStream.shared.emit(const ViewerReloadAssetEvent()); - } - - if (result.count == 0) { - return; - } - - ref.invalidate(localAlbumProvider); - - if (!context.mounted) { - return; - } - - final successMessage = 'delete_local_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - maxWidth: 95.0, - iconData: Icons.no_cell_outlined, - label: "control_bottom_app_bar_delete_from_local".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart deleted file mode 100644 index c02fcf8f79..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'package:flutter/material.dart'; -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/events.model.dart'; -import 'package:immich_mobile/domain/utils/event_stream.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/permanent_delete_dialog.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -/// This delete action has the following behavior: -/// - Delete permanently on the server -/// - Prompt to delete the asset locally -class DeletePermanentActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - final bool useShortLabel; - - const DeletePermanentActionButton({ - super.key, - required this.source, - this.iconOnly = false, - this.menuItem = false, - this.useShortLabel = false, - }); - - Future _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final count = source == ActionSource.viewer ? 1 : ref.read(multiSelectProvider).selectedAssets.length; - final confirm = - await showDialog( - context: context, - builder: (context) => PermanentDeleteDialog(count: count), - ) ?? - false; - if (!confirm) { - return; - } - - if (source == ActionSource.viewer) { - EventStream.shared.emit(const ViewerReloadAssetEvent()); - } - - final result = await ref.read(actionProvider.notifier).deleteRemoteAndLocal(source); - ref.read(multiSelectProvider.notifier).reset(); - if (!context.mounted) { - return; - } - - final successMessage = 'delete_permanently_action_prompt'.t( - context: context, - args: {'count': result.count.toString()}, - ); - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - maxWidth: 110.0, - iconData: Icons.delete_forever, - label: useShortLabel ? "delete".t(context: context) : "delete_permanently".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart deleted file mode 100644 index 3312e4f2a3..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/asset_grid/permanent_delete_dialog.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -/// This delete action has the following behavior: -/// - Delete permanently on the server -/// - Prompt to delete the asset locally -/// -/// This action is used when the asset is selected in multi-selection mode in the trash page -class DeleteTrashActionButton extends ConsumerWidget { - final ActionSource source; - - const DeleteTrashActionButton({super.key, required this.source}); - - Future _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final selectCount = ref.watch(multiSelectProvider.select((s) => s.selectedAssets.length)); - - final confirmDelete = - await showDialog( - context: context, - builder: (context) => PermanentDeleteDialog(count: selectCount), - ) ?? - false; - if (!confirmDelete) { - return; - } - - final result = await ref.read(actionProvider.notifier).deleteRemoteAndLocal(source); - ref.read(multiSelectProvider.notifier).reset(); - if (!context.mounted) { - return; - } - - final successMessage = 'assets_permanently_deleted_count'.t( - context: context, - args: {'count': result.count.toString()}, - ); - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return TextButton.icon( - icon: Icon(Icons.delete_forever, color: Colors.red[400]), - label: Text( - "delete".t(context: context), - style: TextStyle(fontSize: 14, color: Colors.red[400], fontWeight: FontWeight.bold), - ), - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart deleted file mode 100644 index 2be2049a07..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/trash_action_button.widget.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:flutter/material.dart'; -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/events.model.dart'; -import 'package:immich_mobile/domain/utils/event_stream.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -/// This delete action has the following behavior: -/// - Set the deletedAt information, put the asset in the trash in the server -/// which will be permanently deleted after the number of days configure by the admin -class TrashActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const TrashActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - Future _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - if (source == ActionSource.viewer) { - EventStream.shared.emit(const ViewerReloadAssetEvent()); - } - - final result = await ref.read(actionProvider.notifier).trash(source); - ref.read(multiSelectProvider.notifier).reset(); - if (!context.mounted) { - return; - } - - final successMessage = 'trash_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - maxWidth: 85.0, - iconData: Icons.delete_outline_rounded, - label: "control_bottom_app_bar_trash_from_immich".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart index 2819d2502d..33eb917c69 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -1,15 +1,12 @@ import 'package:flutter/material.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/services/timeline.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/presentation/actions/delete.action.dart'; import 'package:immich_mobile/presentation/actions/restore.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; @@ -19,7 +16,6 @@ import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.da import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/utils/semver.dart'; import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart'; import 'package:immich_ui/immich_ui.dart'; @@ -43,8 +39,6 @@ class ViewerBottomBar extends ConsumerWidget { } final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); - final user = ref.watch(currentUserProvider); - final isOwner = asset is RemoteAsset && asset.ownerId == user?.id; final showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails)); final isInLockedView = ref.watch(inLockedViewProvider); final serverInfo = ref.watch(serverInfoProvider); @@ -64,14 +58,7 @@ class ViewerBottomBar extends ConsumerWidget { const EditImageActionButton(), if (asset.hasRemote) AddActionButton(originalTheme: originalTheme), ], - if (isOwner) ...[ - if (asset.isLocalOnly) - const DeleteLocalActionButton(source: ActionSource.viewer) - else if (asset.isTrashed) - const DeletePermanentActionButton(source: ActionSource.viewer, useShortLabel: true) - else - const DeleteActionButton(source: ActionSource.viewer, showConfirmation: true), - ], + ..._actionColumnButtons(context, ref, const [DeleteAction(source: .viewer)]), ], ]; diff --git a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart index 5b041452c0..c8e62c8ec1 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart @@ -5,21 +5,18 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; +import 'package:immich_mobile/presentation/actions/delete.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -48,7 +45,6 @@ class _ArchiveBottomSheetState extends ConsumerState { @override Widget build(BuildContext context) { final multiselect = ref.watch(multiSelectProvider); - final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); Future addToAlbum(RemoteAlbum album) async { final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.timeline, album); @@ -86,15 +82,13 @@ class _ArchiveBottomSheetState extends ConsumerState { const ActionColumnButton(action: ArchiveAction(source: .timeline)), const ActionColumnButton(action: FavoriteAction(source: .timeline)), if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline), - isTrashEnable - ? const TrashActionButton(source: ActionSource.timeline) - : const DeletePermanentActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: DeleteAction(source: .timeline)), const EditDateTimeActionButton(source: ActionSource.timeline), const EditLocationActionButton(source: ActionSource.timeline), const ActionColumnButton(action: LockAction(source: .timeline)), const ActionColumnButton(action: StackAction(source: .timeline)), ], - if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: CleanupLocalAction(source: .timeline)), ], slivers: [ const AddToAlbumHeader(), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart index 7ae0fb6055..ece09d5a76 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart @@ -6,21 +6,18 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; +import 'package:immich_mobile/presentation/actions/delete.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -30,7 +27,6 @@ class FavoriteBottomSheet extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final multiselect = ref.watch(multiSelectProvider); - final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); Future addAssetsToAlbum(RemoteAlbum album) async { final selectedAssets = multiselect.selectedAssets; @@ -86,15 +82,13 @@ class FavoriteBottomSheet extends ConsumerWidget { const ActionColumnButton(action: FavoriteAction(source: .timeline)), const ActionColumnButton(action: ArchiveAction(source: .timeline)), if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline), - isTrashEnable - ? const TrashActionButton(source: ActionSource.timeline) - : const DeletePermanentActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: DeleteAction(source: .timeline)), const EditDateTimeActionButton(source: ActionSource.timeline), const EditLocationActionButton(source: ActionSource.timeline), const ActionColumnButton(action: LockAction(source: .timeline)), const ActionColumnButton(action: StackAction(source: .timeline)), ], - if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: CleanupLocalAction(source: .timeline)), ], slivers: multiselect.hasRemote ? [const AddToAlbumHeader(), AlbumSelector(onAlbumSelected: addAssetsToAlbum)] diff --git a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart index 40e75adf86..c2bf3edf96 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart @@ -5,25 +5,21 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; +import 'package:immich_mobile/presentation/actions/delete.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -52,7 +48,6 @@ class _GeneralBottomSheetState extends ConsumerState { @override Widget build(BuildContext context) { final multiselect = ref.watch(multiSelectProvider); - final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); final tagsEnabled = ref.watch( userMetadataPreferencesProvider.select((value) => value.valueOrNull?.tagsEnabled ?? false), ); @@ -92,9 +87,6 @@ class _GeneralBottomSheetState extends ConsumerState { if (multiselect.hasRemote) ...[ const ShareLinkActionButton(source: ActionSource.timeline), if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline), - isTrashEnable - ? const TrashActionButton(source: ActionSource.timeline) - : const DeletePermanentActionButton(source: ActionSource.timeline), const ActionColumnButton(action: FavoriteAction(source: .timeline)), const ActionColumnButton(action: ArchiveAction(source: .timeline)), if (tagsEnabled) const BulkTagAssetsActionButton(source: ActionSource.timeline), @@ -102,10 +94,9 @@ class _GeneralBottomSheetState extends ConsumerState { const EditLocationActionButton(source: ActionSource.timeline), const ActionColumnButton(action: LockAction(source: .timeline)), const ActionColumnButton(action: StackAction(source: .timeline)), - if (multiselect.onlyLocal || multiselect.hasMerged) const DeleteActionButton(source: ActionSource.timeline), ], - if (multiselect.onlyLocal || multiselect.hasMerged) - const DeleteLocalActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: DeleteAction(source: .timeline)), + const ActionColumnButton(action: CleanupLocalAction(source: .timeline)), if (multiselect.onlyLocal) const UploadActionButton(source: ActionSource.timeline), ], slivers: [ diff --git a/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart index ac8c77af03..dc49f22cfc 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart @@ -3,7 +3,8 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/delete.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; @@ -66,7 +67,8 @@ class _LocalAlbumBottomSheetState extends ConsumerState { shouldCloseOnMinExtent: false, actions: const [ ShareActionButton(source: ActionSource.timeline), - DeleteLocalActionButton(source: ActionSource.timeline), + ActionColumnButton(action: DeleteAction(source: .timeline)), + ActionColumnButton(action: CleanupLocalAction(source: .timeline)), UploadActionButton(source: ActionSource.timeline), ], slivers: [ diff --git a/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart index 9b4fef1a13..e0074c7866 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/delete.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; @@ -20,7 +20,7 @@ class LockedFolderBottomSheet extends ConsumerWidget { actions: [ ShareActionButton(source: ActionSource.timeline), DownloadActionButton(source: ActionSource.timeline), - DeletePermanentActionButton(source: ActionSource.timeline), + ActionColumnButton(action: DeleteAction(source: .timeline)), ActionColumnButton(action: LockAction(source: .timeline)), ], ); diff --git a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart index ac9b85caa1..a9175577b0 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart @@ -5,11 +5,10 @@ import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; +import 'package:immich_mobile/presentation/actions/delete.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; @@ -17,11 +16,9 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_al import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -52,7 +49,6 @@ class _RemoteAlbumBottomSheetState extends ConsumerState @override Widget build(BuildContext context) { final multiselect = ref.watch(multiSelectProvider); - final isTrashEnable = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash)); final ownsAlbum = ref.watch(currentUserProvider)?.id == widget.album.ownerId; Future addToAlbum(RemoteAlbum album) async { @@ -100,16 +96,14 @@ class _RemoteAlbumBottomSheetState extends ConsumerState ], const DownloadActionButton(source: ActionSource.timeline), if (ownsAlbum) ...[ - isTrashEnable - ? const TrashActionButton(source: ActionSource.timeline) - : const DeletePermanentActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: DeleteAction(source: .timeline)), const EditDateTimeActionButton(source: ActionSource.timeline), const EditLocationActionButton(source: ActionSource.timeline), const ActionColumnButton(action: LockAction(source: .timeline)), const ActionColumnButton(action: StackAction(source: .timeline)), ], ], - if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: CleanupLocalAction(source: .timeline)), if (ownsAlbum) RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id), if (ownsAlbum && multiselect.selectedAssets.length == 1) SetAlbumCoverActionButton(source: ActionSource.timeline, albumId: widget.album.id), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart index 4e438884b3..31a36cb970 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/delete.action.dart'; import 'package:immich_mobile/presentation/actions/restore.action.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_trash_action_button.widget.dart'; class TrashBottomBar extends ConsumerWidget { const TrashBottomBar({super.key}); @@ -21,7 +20,7 @@ class TrashBottomBar extends ConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - DeleteTrashActionButton(source: ActionSource.timeline), + ActionColumnButton(action: DeleteAction(source: .timeline)), ActionColumnButton(action: RestoreAction(source: .timeline)), ], ), diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index 20826ddd8e..2222cbf722 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -21,7 +21,6 @@ import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/action.service.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart'; import 'package:immich_mobile/utils/semver.dart'; -import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; @@ -64,24 +63,6 @@ class ActionNotifier extends Notifier { return _getAssets(source).whereType().toIds().toList(growable: false); } - List _getLocalIdsForSource(ActionSource source, {bool ignoreLocalOnly = false}) { - final Set assets = _getAssets(source); - final List localIds = []; - - for (final asset in assets) { - if (ignoreLocalOnly && asset.storage != AssetState.merged) { - continue; - } - if (asset is LocalAsset) { - localIds.add(asset.id); - } else if (asset is RemoteAsset && asset.localId != null) { - localIds.add(asset.localId!); - } - } - - return localIds; - } - List _getOwnedRemoteIdsForSource(ActionSource source) { final ownerId = ref.read(currentUserProvider)?.id; return _getAssets(source).whereType().ownedAssets(ownerId).toIds().toList(growable: false); @@ -118,18 +99,6 @@ class ActionNotifier extends Notifier { } } - Future trash(ActionSource source) async { - final ids = _getOwnedRemoteIdsForSource(source); - - try { - await _service.trash(ids); - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to trash assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - Future emptyTrash(String userId) async { try { final count = await _service.emptyTrash(userId); @@ -150,60 +119,6 @@ class ActionNotifier extends Notifier { } } - Future trashRemoteAndDeleteLocal(ActionSource source) async { - final ids = _getOwnedRemoteIdsForSource(source); - final localIds = _getLocalIdsForSource(source); - try { - await _service.trashRemoteAndDeleteLocal(ids, localIds); - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to delete assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - - Future deleteRemoteAndLocal(ActionSource source) async { - final ids = _getOwnedRemoteIdsForSource(source); - final localIds = _getLocalIdsForSource(source); - try { - await _service.deleteRemoteAndLocal(ids, localIds); - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to delete assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - - Future deleteLocal(ActionSource source, BuildContext context) async { - final assets = _getAssets(source); - final bool? backedUpOnly = assets.every((asset) => asset.storage == AssetState.merged) - ? true - : await showDialog( - context: context, - builder: (BuildContext context) => DeleteLocalOnlyDialog(onDeleteLocal: (_) {}), - ); - - if (backedUpOnly == null) { - // User cancelled the dialog - return null; - } - - final List ids; - if (backedUpOnly) { - ids = assets.where((asset) => asset.storage == AssetState.merged).map((asset) => asset.localId!).toList(); - } else { - ids = _getLocalIdsForSource(source); - } - - try { - final deletedCount = await _service.deleteLocal(ids); - return ActionResult(count: deletedCount, success: true); - } catch (error, stack) { - _logger.severe('Failed to delete assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - Future editLocation(ActionSource source, BuildContext context) async { final ids = _getOwnedRemoteIdsForSource(source); try { diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index 00b608c8a6..28e2d27c7e 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -6,14 +6,9 @@ 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/asset_edit.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/tag.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/extensions/platform_extensions.dart'; -import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; @@ -31,10 +26,8 @@ final actionServiceProvider = Provider( (ref) => ActionService( ref.watch(assetApiRepositoryProvider), ref.watch(remoteAssetRepositoryProvider), - ref.watch(localAssetRepository), ref.watch(driftAlbumApiRepositoryProvider), ref.watch(remoteAlbumRepository), - ref.watch(trashedLocalAssetRepository), ref.watch(assetMediaRepositoryProvider), ref.watch(downloadRepositoryProvider), ref.watch(tagServiceProvider), @@ -44,10 +37,8 @@ final actionServiceProvider = Provider( class ActionService { final AssetApiRepository _assetApiRepository; final RemoteAssetRepository _remoteAssetRepository; - final DriftLocalAssetRepository _localAssetRepository; final DriftAlbumApiRepository _albumApiRepository; final DriftRemoteAlbumRepository _remoteAlbumRepository; - final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository; final AssetMediaRepository _assetMediaRepository; final DownloadRepository _downloadRepository; final TagService _tagService; @@ -55,10 +46,8 @@ class ActionService { const ActionService( this._assetApiRepository, this._remoteAssetRepository, - this._localAssetRepository, this._albumApiRepository, this._remoteAlbumRepository, - this._trashedLocalAssetRepository, this._assetMediaRepository, this._downloadRepository, this._tagService, @@ -68,11 +57,6 @@ class ActionService { unawaited(context.pushRoute(SharedLinkEditRoute(assetsList: remoteIds))); } - Future trash(List remoteIds) async { - await _assetApiRepository.delete(remoteIds, false); - await _remoteAssetRepository.trash(remoteIds); - } - Future emptyTrash(String userId) async { final count = await _assetApiRepository.emptyTrash(); await _remoteAssetRepository.emptyTrash(userId); @@ -85,28 +69,6 @@ class ActionService { return count; } - Future trashRemoteAndDeleteLocal(List remoteIds, List localIds) async { - await _assetApiRepository.delete(remoteIds, false); - await _remoteAssetRepository.trash(remoteIds); - - if (localIds.isNotEmpty) { - await _deleteLocalAssets(localIds); - } - } - - Future deleteRemoteAndLocal(List remoteIds, List localIds) async { - await _assetApiRepository.delete(remoteIds, true); - await _remoteAssetRepository.delete(remoteIds); - - if (localIds.isNotEmpty) { - await _deleteLocalAssets(localIds); - } - } - - Future deleteLocal(List localIds) async { - return await _deleteLocalAssets(localIds); - } - Future editLocation(List remoteIds, BuildContext context) async { maplibre.LatLng? initialLatLng; if (remoteIds.length == 1) { @@ -270,17 +232,4 @@ class ActionService { await _assetApiRepository.editAsset(remoteId, edits); } } - - Future _deleteLocalAssets(List localIds) async { - final deletedIds = await _assetMediaRepository.deleteAll(localIds); - if (deletedIds.isEmpty) { - return 0; - } - if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) { - await _trashedLocalAssetRepository.applyTrashedAssets(deletedIds); - } else { - await _localAssetRepository.delete(deletedIds); - } - return deletedIds.length; - } } diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 2924907423..b8d231e436 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -10,14 +10,12 @@ import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; +import 'package:immich_mobile/presentation/actions/delete.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/actions/restore.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart'; @@ -28,7 +26,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_b import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/slideshow_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -83,9 +80,7 @@ enum ActionButtonType { removeFromLockFolder, removeFromAlbum, restoreTrash, - trash, deleteLocal, - deletePermanent, delete, advancedInfo; @@ -110,25 +105,12 @@ enum ActionButtonType { !context.isInLockedView && // context.asset.hasRemote && // !context.asset.hasLocal, - ActionButtonType.trash => - context.isOwner && // - !context.isInLockedView && // - context.asset.hasRemote && // - context.isTrashEnabled && // - context.timelineOrigin != TimelineOrigin.trash, ActionButtonType.restoreTrash => context.isOwner && // !context.isInLockedView && // context.asset.hasRemote && // context.timelineOrigin == TimelineOrigin.trash, - ActionButtonType.deletePermanent => - context.isOwner && // - context.asset.hasRemote && // - (!context.isTrashEnabled || context.timelineOrigin == TimelineOrigin.trash || context.isInLockedView), - ActionButtonType.delete => - context.isOwner && // - !context.isInLockedView && // - context.asset.hasRemote, + ActionButtonType.delete => true, ActionButtonType.moveToLockFolder => context.isOwner && // !context.isInLockedView && // @@ -139,7 +121,7 @@ enum ActionButtonType { context.asset.hasRemote, ActionButtonType.deleteLocal => !context.isInLockedView && // - context.asset.hasLocal, + context.asset.isMerged, ActionButtonType.upload => !context.isInLockedView && // context.asset.storage == AssetState.local, @@ -201,21 +183,11 @@ enum ActionButtonType { ActionButtonType.archive || ActionButtonType.unarchive => ActionMenuItem(action: ArchiveAction(source: context.source)), ActionButtonType.download => DownloadActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), - ActionButtonType.trash => TrashActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), ActionButtonType.restoreTrash => ActionMenuItem(action: RestoreAction(source: context.source)), - ActionButtonType.deletePermanent => DeletePermanentActionButton( - source: context.source, - iconOnly: iconOnly, - menuItem: menuItem, - ), - ActionButtonType.delete => DeleteActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), + ActionButtonType.delete => ActionMenuItem(action: DeleteAction(source: context.source)), ActionButtonType.moveToLockFolder || ActionButtonType.removeFromLockFolder => ActionMenuItem(action: LockAction(source: context.source)), - ActionButtonType.deleteLocal => DeleteLocalActionButton( - source: context.source, - iconOnly: iconOnly, - menuItem: menuItem, - ), + ActionButtonType.deleteLocal => ActionMenuItem(action: CleanupLocalAction(source: context.source)), ActionButtonType.upload => UploadActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), ActionButtonType.removeFromAlbum => RemoveFromAlbumActionButton( albumId: context.currentAlbum!.id, @@ -276,8 +248,6 @@ enum ActionButtonType { // 0: info ActionButtonType.openInfo => 0, // 10: move, remove, and delete - ActionButtonType.trash => 10, - ActionButtonType.deletePermanent => 10, ActionButtonType.removeFromLockFolder => 10, ActionButtonType.removeFromAlbum => 10, ActionButtonType.unstack => 10, @@ -305,7 +275,6 @@ class ActionButtonBuilder { ActionButtonType.archive, ActionButtonType.unarchive, ActionButtonType.restoreTrash, - ActionButtonType.deletePermanent, }; static List build(ActionButtonContext context) { diff --git a/mobile/test/services/action.service_test.dart b/mobile/test/services/action.service_test.dart index 72691a3802..76f02e29d0 100644 --- a/mobile/test/services/action.service_test.dart +++ b/mobile/test/services/action.service_test.dart @@ -2,7 +2,6 @@ import 'package:drift/drift.dart' as drift; import 'package:drift/native.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; @@ -18,10 +17,8 @@ void main() { late MockAssetApiRepository assetApiRepository; late MockRemoteAssetRepository remoteAssetRepository; - late MockDriftLocalAssetRepository localAssetRepository; late MockDriftAlbumApiRepository albumApiRepository; late MockRemoteAlbumRepository remoteAlbumRepository; - late MockTrashedLocalAssetRepository trashedLocalAssetRepository; late MockAssetMediaRepository assetMediaRepository; late MockDownloadRepository downloadRepository; late MockTagService tagService; @@ -45,10 +42,8 @@ void main() { setUp(() { assetApiRepository = MockAssetApiRepository(); remoteAssetRepository = MockRemoteAssetRepository(); - localAssetRepository = MockDriftLocalAssetRepository(); albumApiRepository = MockDriftAlbumApiRepository(); remoteAlbumRepository = MockRemoteAlbumRepository(); - trashedLocalAssetRepository = MockTrashedLocalAssetRepository(); assetMediaRepository = MockAssetMediaRepository(); downloadRepository = MockDownloadRepository(); tagService = MockTagService(); @@ -56,10 +51,8 @@ void main() { sut = ActionService( assetApiRepository, remoteAssetRepository, - localAssetRepository, albumApiRepository, remoteAlbumRepository, - trashedLocalAssetRepository, assetMediaRepository, downloadRepository, tagService, @@ -138,50 +131,4 @@ void main() { verify(() => remoteAssetRepository.updateDateTime(ids, DateTime.parse(picked), timeZone: null)).called(1); }); }); - - group('ActionService.deleteLocal', () { - test('routes deleted ids to trashed repository when Android trash handling is enabled', () async { - await Store.put(StoreKey.manageLocalMediaAndroid, true); - const ids = ['a', 'b']; - - when(() => assetMediaRepository.deleteAll(ids)).thenAnswer((_) async => ids); - when(() => trashedLocalAssetRepository.applyTrashedAssets(ids)).thenAnswer((_) async {}); - - final result = await sut.deleteLocal(ids); - - expect(result, ids.length); - verify(() => assetMediaRepository.deleteAll(ids)).called(1); - verify(() => trashedLocalAssetRepository.applyTrashedAssets(ids)).called(1); - verifyNever(() => localAssetRepository.delete(any())); - }); - - test('deletes locally when Android trash handling is disabled', () async { - await Store.put(StoreKey.manageLocalMediaAndroid, false); - const ids = ['c']; - - when(() => assetMediaRepository.deleteAll(ids)).thenAnswer((_) async => ids); - when(() => localAssetRepository.delete(ids)).thenAnswer((_) async {}); - - final result = await sut.deleteLocal(ids); - - expect(result, ids.length); - verify(() => assetMediaRepository.deleteAll(ids)).called(1); - verify(() => localAssetRepository.delete(ids)).called(1); - verifyNever(() => trashedLocalAssetRepository.applyTrashedAssets(any())); - }); - - test('short-circuits when nothing was deleted', () async { - await Store.put(StoreKey.manageLocalMediaAndroid, true); - const ids = ['x']; - - when(() => assetMediaRepository.deleteAll(ids)).thenAnswer((_) async => []); - - final result = await sut.deleteLocal(ids); - - expect(result, 0); - verify(() => assetMediaRepository.deleteAll(ids)).called(1); - verifyNever(() => trashedLocalAssetRepository.applyTrashedAssets(any())); - verifyNever(() => localAssetRepository.delete(any())); - }); - }); } diff --git a/mobile/test/unit/presentation/actions/delete_action_test.dart b/mobile/test/unit/presentation/actions/delete_action_test.dart new file mode 100644 index 0000000000..4b75b01eaf --- /dev/null +++ b/mobile/test/unit/presentation/actions/delete_action_test.dart @@ -0,0 +1,257 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/delete.action.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../service.mocks.dart'; +import '../../factories/local_asset_factory.dart'; +import '../../factories/remote_asset_factory.dart'; +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + late MockAssetService assetService; + late MockCleanupService cleanupService; + + setUp(() async { + context = await PresentationContext.create(); + assetService = context.service.asset.service; + cleanupService = context.service.cleanup.service; + }); + + tearDown(() async { + debugDefaultTargetPlatformOverride = null; + await StoreService.I.put(StoreKey.manageLocalMediaAndroid, false); + context.dispose(); + }); + + RemoteAsset owned({AssetVisibility visibility = .timeline, DateTime? deletedAt, String? localId}) => + RemoteAssetFactory.create( + ownerId: context.currentUser.id, + visibility: visibility, + deletedAt: deletedAt, + localId: localId, + ); + + Future pumpDelete(WidgetTester tester, Set selection, {bool trashEnabled = true}) async { + if (!trashEnabled) { + when( + () => context.service.serverInfo.getServerFeatures(), + ).thenAnswer((_) async => const .new(trash: false, map: true, oauthEnabled: false, passwordLogin: true)); + } + + await tester.pumpTestWidget( + context, + const ActionIconButton(action: DeleteAction(source: .timeline)), + overrides: context.selected(selection), + ); + + if (!trashEnabled) { + final scope = ProviderScope.containerOf(tester.element(find.byType(ActionIconButton)), listen: false); + await scope.read(serverInfoProvider.notifier).getServerFeatures(); + await tester.pumpAndSettle(); + } + + await tester.tap(find.byType(ImmichIconButton)); + await tester.pump(); + } + + Future respondToDialog(WidgetTester tester, {required bool confirm}) async { + await tester.pump(const Duration(milliseconds: 300)); + expect(find.byType(ConfirmDialog), findsOneWidget); + await tester.tap(find.byType(TextButton).at(confirm ? 1 : 0)); // [cancel, ok] + await tester.pumpAndSettle(); + } + + group('DeleteAction', () { + group('trash', () { + testWidgets('trashes a remote-only owned asset', (tester) async { + final asset = owned(); + + await pumpDelete(tester, {asset}); + await tester.pumpAndSettle(); + + verify(() => assetService.trash([asset.id])).called(1); + verifyNever(() => assetService.delete(any())); + verifyNever(() => cleanupService.deleteLocalAssets(any())); + }); + + testWidgets('ignores assets owned by someone else', (tester) async { + final mine = owned(); + final theirs = RemoteAssetFactory.create(); + + await pumpDelete(tester, {mine, theirs}); + await tester.pumpAndSettle(); + + verify(() => assetService.trash([mine.id])).called(1); + }); + + testWidgets('trashes a merged asset and removes its device copy', (tester) async { + final asset = owned(localId: 'local'); + + await pumpDelete(tester, {asset}); + await tester.pumpAndSettle(); + + verify(() => cleanupService.deleteLocalAssets(['local'])).called(1); + verify(() => assetService.trash([asset.id])).called(1); + }); + }); + + group('permanent', () { + testWidgets('permanently deletes when the trash feature is disabled', (tester) async { + final asset = owned(); + + await pumpDelete(tester, {asset}, trashEnabled: false); + await respondToDialog(tester, confirm: true); + + verify(() => assetService.delete([asset.id])).called(1); + verifyNever(() => assetService.trash(any())); + }); + + testWidgets('permanently deletes a merged asset and removes its device copy', (tester) async { + final asset = owned(localId: 'local'); + + await pumpDelete(tester, {asset}, trashEnabled: false); + await respondToDialog(tester, confirm: true); + + verify(() => assetService.delete([asset.id])).called(1); + verify(() => cleanupService.deleteLocalAssets(['local'])).called(1); + }); + + testWidgets('permanently deletes already trashed assets even with trash enabled', (tester) async { + final asset = owned(deletedAt: DateTime(2024)); + + await pumpDelete(tester, {asset}); + await respondToDialog(tester, confirm: true); + + verify(() => assetService.delete([asset.id])).called(1); + verifyNever(() => assetService.trash(any())); + }); + + testWidgets('permanently deletes locked folder assets even with trash enabled', (tester) async { + final asset = owned(visibility: .locked, localId: 'local'); + + await pumpDelete(tester, {asset}); + await respondToDialog(tester, confirm: true); + + verify(() => assetService.delete([asset.id])).called(1); + verify(() => cleanupService.deleteLocalAssets(['local'])).called(1); + }); + + testWidgets('does nothing when the confirmation is cancelled', (tester) async { + final asset = owned(visibility: .locked, localId: 'local'); + + await pumpDelete(tester, {asset}); + await respondToDialog(tester, confirm: false); + + verifyNever(() => assetService.delete(any())); + verifyNever(() => cleanupService.deleteLocalAssets(any())); + }); + }); + + group('local only', () { + testWidgets('removes the device copy with no remote call', (tester) async { + final asset = LocalAssetFactory.create(); + + await pumpDelete(tester, {asset}); + await tester.pumpAndSettle(); + + verify(() => cleanupService.deleteLocalAssets([asset.id])).called(1); + verifyNever(() => assetService.trash(any())); + verifyNever(() => assetService.delete(any())); + }); + }); + + group('prompt handling', () { + testWidgets('permanent delete shows a single app dialog', (tester) async { + final asset = owned(localId: 'local'); + + await pumpDelete(tester, {asset}, trashEnabled: false); + await tester.pump(const Duration(milliseconds: 300)); + + expect(find.text(StaticTranslations.instance.delete_dialog_title), findsOneWidget); + await tester.tap(find.byType(TextButton).at(1)); + await tester.pumpAndSettle(); + + expect(find.text(StaticTranslations.instance.move_to_device_trash), findsNothing); + verify(() => assetService.delete([asset.id])).called(1); + verify(() => cleanupService.deleteLocalAssets(['local'])).called(1); + }); + + testWidgets('local only delete on Android with MANAGE_MEDIA shows the prompt', (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + await StoreService.I.put(StoreKey.manageLocalMediaAndroid, true); + final asset = LocalAssetFactory.create(); + + await pumpDelete(tester, {asset}); + await tester.pump(const Duration(milliseconds: 300)); + + expect(find.text(StaticTranslations.instance.move_to_device_trash), findsOneWidget); + await tester.tap(find.byType(TextButton).at(1)); // confirm + await tester.pumpAndSettle(); + + verify(() => cleanupService.deleteLocalAssets([asset.id])).called(1); + // Has to be cleared inside the body; the framework asserts on it before tearDown runs. + debugDefaultTargetPlatformOverride = null; + }); + + testWidgets('local only delete on Android with MANAGE_MEDIA deletes nothing when cancelled', (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + await StoreService.I.put(StoreKey.manageLocalMediaAndroid, true); + final asset = LocalAssetFactory.create(); + + await pumpDelete(tester, {asset}); + await respondToDialog(tester, confirm: false); + + verifyNever(() => cleanupService.deleteLocalAssets(any())); + debugDefaultTargetPlatformOverride = null; + }); + }); + + testWidgets('is hidden when nothing can be deleted', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: DeleteAction(source: .timeline)), + overrides: context.selected({RemoteAssetFactory.create()}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); + + group('CleanupLocalAction', () { + testWidgets('deletes only backed up device copies', (tester) async { + final backedUp = LocalAssetFactory.create(remoteId: 'remote'); + final localOnly = LocalAssetFactory.create(); + + await tester.pumpTestAction( + context, + const CleanupLocalAction(source: .timeline), + overrides: context.selected({backedUp, localOnly}), + ); + await tester.pumpAndSettle(); + + verify(() => cleanupService.deleteLocalAssets([backedUp.id])).called(1); + }); + + testWidgets('is hidden when no backed up assets are selected', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: CleanupLocalAction(source: .timeline)), + overrides: context.selected({LocalAssetFactory.create()}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); +} diff --git a/mobile/test/unit/presentation/presentation_context.dart b/mobile/test/unit/presentation/presentation_context.dart index a45c1c14f9..2e5871a419 100644 --- a/mobile/test/unit/presentation/presentation_context.dart +++ b/mobile/test/unit/presentation/presentation_context.dart @@ -19,6 +19,7 @@ import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/services/cleanup.service.dart'; import 'package:immich_mobile/services/gcast.service.dart'; import 'package:immich_mobile/services/server_info.service.dart'; import 'package:immich_ui/immich_ui.dart'; @@ -48,6 +49,7 @@ class PresentationContext { List get overrides => [ currentUserProvider.overrideWith((ref) => CurrentUserProvider(service.user.service)), assetServiceProvider.overrideWithValue(service.asset.service), + cleanupServiceProvider.overrideWithValue(service.cleanup.service), partnerServiceProvider.overrideWithValue(service.partner.service), gCastServiceProvider.overrideWithValue(service.cast), serverInfoServiceProvider.overrideWithValue(service.serverInfo), diff --git a/mobile/test/utils_legacy/action_button_utils_test.dart b/mobile/test/utils_legacy/action_button_utils_test.dart index 52d25c4c75..65e1cad7f7 100644 --- a/mobile/test/utils_legacy/action_button_utils_test.dart +++ b/mobile/test/utils_legacy/action_button_utils_test.dart @@ -427,60 +427,6 @@ void main() { }); }); - group('trash button', () { - test('should show when owner, not locked, has remote, and trash enabled', () { - final remoteAsset = createRemoteAsset(); - final context = ActionButtonContext( - asset: remoteAsset, - isOwner: true, - isArchived: false, - isTrashEnabled: true, - isInLockedView: false, - currentAlbum: null, - advancedTroubleshooting: false, - isStacked: false, - source: ActionSource.timeline, - ); - - expect(ActionButtonType.trash.shouldShow(context), isTrue); - }); - - test('should not show when trash disabled', () { - final remoteAsset = createRemoteAsset(); - final context = ActionButtonContext( - asset: remoteAsset, - isOwner: true, - isArchived: false, - isTrashEnabled: false, - isInLockedView: false, - currentAlbum: null, - advancedTroubleshooting: false, - isStacked: false, - source: ActionSource.timeline, - ); - - expect(ActionButtonType.trash.shouldShow(context), isFalse); - }); - - test('should not show when asset is already trashed', () { - final remoteAsset = createRemoteAsset(deletedAt: DateTime(2024)); - final context = ActionButtonContext( - asset: remoteAsset, - isOwner: true, - isArchived: false, - isTrashEnabled: true, - isInLockedView: false, - currentAlbum: null, - advancedTroubleshooting: false, - isStacked: false, - source: ActionSource.viewer, - timelineOrigin: TimelineOrigin.trash, - ); - - expect(ActionButtonType.trash.shouldShow(context), isFalse); - }); - }); - group('restoreTrash button', () { test('should show when owner, not locked, has remote, and is in trash timeline', () { final remoteAsset = createRemoteAsset(); @@ -519,60 +465,6 @@ void main() { }); }); - group('deletePermanent button', () { - test('should show when owner, not locked, has remote, and trash disabled', () { - final remoteAsset = createRemoteAsset(); - final context = ActionButtonContext( - asset: remoteAsset, - isOwner: true, - isArchived: false, - isTrashEnabled: false, - isInLockedView: false, - currentAlbum: null, - advancedTroubleshooting: false, - isStacked: false, - source: ActionSource.timeline, - ); - - expect(ActionButtonType.deletePermanent.shouldShow(context), isTrue); - }); - - test('should not show when trash enabled', () { - final remoteAsset = createRemoteAsset(); - final context = ActionButtonContext( - asset: remoteAsset, - isOwner: true, - isArchived: false, - isTrashEnabled: true, - isInLockedView: false, - currentAlbum: null, - advancedTroubleshooting: false, - isStacked: false, - source: ActionSource.timeline, - ); - - expect(ActionButtonType.deletePermanent.shouldShow(context), isFalse); - }); - - test('should show when asset is trashed even with trash enabled', () { - final remoteAsset = createRemoteAsset(deletedAt: DateTime(2024)); - final context = ActionButtonContext( - asset: remoteAsset, - isOwner: true, - isArchived: false, - isTrashEnabled: true, - isInLockedView: false, - currentAlbum: null, - advancedTroubleshooting: false, - isStacked: false, - source: ActionSource.viewer, - timelineOrigin: TimelineOrigin.trash, - ); - - expect(ActionButtonType.deletePermanent.shouldShow(context), isTrue); - }); - }); - group('delete button', () { test('should show when owner, not locked, and has remote', () { final remoteAsset = createRemoteAsset(); @@ -612,7 +504,7 @@ void main() { }); group('deleteLocal button', () { - test('should show when not locked and asset is local only', () { + test('should not show when asset is local only, as there is no backup to fall back on', () { final localAsset = createLocalAsset(); final context = ActionButtonContext( asset: localAsset, @@ -626,7 +518,7 @@ void main() { source: ActionSource.timeline, ); - expect(ActionButtonType.deleteLocal.shouldShow(context), isTrue); + expect(ActionButtonType.deleteLocal.shouldShow(context), isFalse); }); test('should not show when asset is not local only', () { From 36f12ec805f7c181ead39f3603ac9402e1d91b31 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Sat, 1 Aug 2026 08:06:41 +0530 Subject: [PATCH 36/81] refactor: mobile open in browser, similar, set profile action (#29769) refactor: mobile browser and similar photos action Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../actions/open_in_browser.action.dart | 35 +++++++++++ .../actions/set_profile_picture.action.dart | 22 +++++++ .../actions/similar_photos.action.dart | 42 ++++++++++++++ .../open_in_browser_action_button.widget.dart | 55 ------------------ ..._profile_picture_action_button.widget.dart | 37 ------------ .../similar_photos_action_button.widget.dart | 58 ------------------- mobile/lib/utils/action_button.utils.dart | 25 +++----- .../actions/open_in_browser_action_test.dart | 48 +++++++++++++++ 8 files changed, 155 insertions(+), 167 deletions(-) create mode 100644 mobile/lib/presentation/actions/open_in_browser.action.dart create mode 100644 mobile/lib/presentation/actions/set_profile_picture.action.dart create mode 100644 mobile/lib/presentation/actions/similar_photos.action.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart create mode 100644 mobile/test/unit/presentation/actions/open_in_browser_action_test.dart diff --git a/mobile/lib/presentation/actions/open_in_browser.action.dart b/mobile/lib/presentation/actions/open_in_browser.action.dart new file mode 100644 index 0000000000..6b05762977 --- /dev/null +++ b/mobile/lib/presentation/actions/open_in_browser.action.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class OpenInBrowserAction extends ActionBuilder { + final String remoteId; + final TimelineOrigin origin; + + const OpenInBrowserAction({required this.remoteId, required this.origin}); + + @override + ActionItem create(BuildContext context, WidgetRef ref) => + .new(icon: Icons.open_in_browser, label: context.t.open_in_browser, onAction: () => _open(ref)); + + Future _open(WidgetRef ref) async { + final serverEndpoint = ref.read(storeServiceProvider).get(.serverEndpoint).replaceFirst('/api', ''); + final url = Uri.parse('$serverEndpoint${webPathFor(origin)}/photos/$remoteId'); + + if (await canLaunchUrl(url)) { + await launchUrl(url, mode: .externalApplication); + } + } +} + +@visibleForTesting +String webPathFor(TimelineOrigin origin) => switch (origin) { + .favorite => '/favorites', + .trash => '/trash', + .archive => '/archive', + _ => '', +}; diff --git a/mobile/lib/presentation/actions/set_profile_picture.action.dart b/mobile/lib/presentation/actions/set_profile_picture.action.dart new file mode 100644 index 0000000000..c14be1dc87 --- /dev/null +++ b/mobile/lib/presentation/actions/set_profile_picture.action.dart @@ -0,0 +1,22 @@ +import 'dart:async'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/routing/router.dart'; + +class SetProfilePictureAction extends ActionBuilder { + final BaseAsset asset; + + const SetProfilePictureAction({required this.asset}); + + @override + ActionItem create(BuildContext context, WidgetRef ref) => .new( + icon: Icons.account_circle_outlined, + label: context.t.set_as_profile_picture, + onAction: () async => unawaited(context.pushRoute(ProfilePictureCropRoute(asset: asset))), + ); +} diff --git a/mobile/lib/presentation/actions/similar_photos.action.dart b/mobile/lib/presentation/actions/similar_photos.action.dart new file mode 100644 index 0000000000..3c0b7b129b --- /dev/null +++ b/mobile/lib/presentation/actions/similar_photos.action.dart @@ -0,0 +1,42 @@ +import 'dart:async'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; + +class SimilarPhotosAction extends ActionBuilder { + final String assetId; + + const SimilarPhotosAction({required this.assetId}); + + @override + ActionItem create(BuildContext context, WidgetRef ref) => + .new(icon: Icons.compare, label: context.t.view_similar_photos, onAction: () => _search(context, ref)); + + Future _search(BuildContext context, WidgetRef ref) async { + ref.invalidate(assetViewerProvider); + ref.invalidate(paginatedSearchProvider); + + ref.read(searchPreFilterProvider.notifier) + ..clear() + ..setFilter( + .new( + assetId: assetId, + people: {}, + location: .new(), + camera: .new(), + date: .new(), + display: .new(isNotInAlbum: false, isArchive: false, isFavorite: false), + rating: .new(), + mediaType: .other, + ), + ); + + unawaited(context.navigateTo(const DriftSearchRoute())); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart deleted file mode 100644 index adf73e4107..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/timeline.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class OpenInBrowserActionButton extends ConsumerWidget { - final String remoteId; - final TimelineOrigin origin; - final bool iconOnly; - final bool menuItem; - - const OpenInBrowserActionButton({ - super.key, - required this.remoteId, - required this.origin, - this.iconOnly = false, - this.menuItem = false, - }); - - Future _onTap() async { - final serverEndpoint = Store.get(StoreKey.serverEndpoint).replaceFirst('/api', ''); - - String originPath = ''; - switch (origin) { - case TimelineOrigin.favorite: - originPath = '/favorites'; - case TimelineOrigin.trash: - originPath = '/trash'; - case TimelineOrigin.archive: - originPath = '/archive'; - default: - break; - } - - final url = '$serverEndpoint$originPath/photos/$remoteId'; - if (await canLaunchUrl(Uri.parse(url))) { - await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - label: 'open_in_browser'.t(context: context), - iconData: Icons.open_in_browser, - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: _onTap, - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart deleted file mode 100644 index 5b41715022..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/routing/router.dart'; - -class SetProfilePictureActionButton extends ConsumerWidget { - final BaseAsset asset; - final bool iconOnly; - final bool menuItem; - - const SetProfilePictureActionButton({super.key, required this.asset, this.iconOnly = false, this.menuItem = false}); - - Future _onTap(BuildContext context) async { - if (!context.mounted) { - return; - } - - await context.pushRoute(ProfilePictureCropRoute(asset: asset)); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.account_circle_outlined, - label: "set_as_profile_picture".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => unawaited(_onTap(context)), - maxWidth: 100, - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart deleted file mode 100644 index 02da265f31..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/models/search/search_filter.model.dart'; -import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; - -class SimilarPhotosActionButton extends ConsumerWidget { - final String assetId; - final bool iconOnly; - final bool menuItem; - - const SimilarPhotosActionButton({super.key, required this.assetId, this.iconOnly = false, this.menuItem = false}); - - Future _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - ref.invalidate(assetViewerProvider); - ref.invalidate(paginatedSearchProvider); - - ref.read(searchPreFilterProvider.notifier) - ..clear() - ..setFilter( - SearchFilter( - assetId: assetId, - people: {}, - location: SearchLocationFilter(), - camera: SearchCameraFilter(), - date: SearchDateFilter(), - display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false), - rating: SearchRatingFilter(), - mediaType: AssetType.other, - ), - ); - - unawaited(context.navigateTo(const DriftSearchRoute())); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.compare, - label: "view_similar_photos".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - maxWidth: 100, - ); - } -} diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index b8d231e436..32aaf360d0 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -12,19 +12,19 @@ import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; +import 'package:immich_mobile/presentation/actions/open_in_browser.action.dart'; import 'package:immich_mobile/presentation/actions/restore.action.dart'; +import 'package:immich_mobile/presentation/actions/set_profile_picture.action.dart'; +import 'package:immich_mobile/presentation/actions/similar_photos.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/open_in_browser_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/slideshow_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -203,22 +203,13 @@ enum ActionButtonType { ), ActionButtonType.likeActivity => LikeActivityActionButton(iconOnly: iconOnly, menuItem: menuItem), ActionButtonType.unstack => ActionMenuItem(action: StackAction(source: context.source)), - ActionButtonType.openInBrowser => OpenInBrowserActionButton( - remoteId: context.asset.remoteId!, - origin: context.timelineOrigin, - iconOnly: iconOnly, - menuItem: menuItem, + ActionButtonType.openInBrowser => ActionMenuItem( + action: OpenInBrowserAction(remoteId: context.asset.remoteId!, origin: context.timelineOrigin), ), - ActionButtonType.similarPhotos => SimilarPhotosActionButton( - assetId: (context.asset as RemoteAsset).id, - iconOnly: iconOnly, - menuItem: menuItem, - ), - ActionButtonType.setProfilePicture => SetProfilePictureActionButton( - asset: context.asset, - iconOnly: iconOnly, - menuItem: menuItem, + ActionButtonType.similarPhotos => ActionMenuItem( + action: SimilarPhotosAction(assetId: (context.asset as RemoteAsset).id), ), + ActionButtonType.setProfilePicture => ActionMenuItem(action: SetProfilePictureAction(asset: context.asset)), ActionButtonType.openInfo => BaseActionButton( label: 'info'.tr(), iconData: Icons.info_outline, diff --git a/mobile/test/unit/presentation/actions/open_in_browser_action_test.dart b/mobile/test/unit/presentation/actions/open_in_browser_action_test.dart new file mode 100644 index 0000000000..0ed99e7704 --- /dev/null +++ b/mobile/test/unit/presentation/actions/open_in_browser_action_test.dart @@ -0,0 +1,48 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/open_in_browser.action.dart'; +import 'package:immich_ui/immich_ui.dart'; + +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + + setUp(() async { + context = await PresentationContext.create(); + }); + + tearDown(() { + context.dispose(); + }); + + group('webPathFor', () { + const dedicatedPages = { + TimelineOrigin.favorite: '/favorites', + TimelineOrigin.trash: '/trash', + TimelineOrigin.archive: '/archive', + }; + + for (final origin in TimelineOrigin.values) { + final expected = dedicatedPages[origin] ?? ''; + + test('opens ${origin.name} on ${expected.isEmpty ? 'the main timeline' : expected}', () { + expect(webPathFor(origin), expected); + }); + } + }); + + group('OpenInBrowserAction', () { + testWidgets('always renders, since the kebab menu decides whether to offer it', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton( + action: OpenInBrowserAction(remoteId: 'remote-1', origin: .main), + ), + ); + + expect(find.byType(ImmichIconButton), findsOneWidget); + }); + }); +} From a048e86217b5bfbbf8c0ba388e390268cfb7d6cf Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Sat, 1 Aug 2026 08:06:42 +0530 Subject: [PATCH 37/81] refactor: mobile cast and slideshow action (#29768) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../lib/presentation/actions/cast.action.dart | 23 ++++++++ .../actions/slideshow.action.dart | 21 ++++++++ .../cast_action_button.widget.dart | 32 ------------ .../slideshow_action_button.widget.dart | 36 ------------- mobile/lib/utils/action_button.utils.dart | 8 +-- .../actions/cast_action_test.dart | 52 +++++++++++++++++++ 6 files changed, 100 insertions(+), 72 deletions(-) create mode 100644 mobile/lib/presentation/actions/cast.action.dart create mode 100644 mobile/lib/presentation/actions/slideshow.action.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/slideshow_action_button.widget.dart create mode 100644 mobile/test/unit/presentation/actions/cast_action_test.dart diff --git a/mobile/lib/presentation/actions/cast.action.dart b/mobile/lib/presentation/actions/cast.action.dart new file mode 100644 index 0000000000..5d90479647 --- /dev/null +++ b/mobile/lib/presentation/actions/cast.action.dart @@ -0,0 +1,23 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/cast.provider.dart'; +import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart'; + +class CastAction extends ActionBuilder { + const CastAction(); + + @override + ActionItem create(BuildContext context, WidgetRef ref) { + final isCasting = ref.watch(castProvider.select((state) => state.isCasting)); + + return .new( + icon: isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded, + label: context.t.cast, + onAction: () async => unawaited(showDialog(context: context, builder: (_) => const CastDialog())), + ); + } +} diff --git a/mobile/lib/presentation/actions/slideshow.action.dart b/mobile/lib/presentation/actions/slideshow.action.dart new file mode 100644 index 0000000000..4b6c90b517 --- /dev/null +++ b/mobile/lib/presentation/actions/slideshow.action.dart @@ -0,0 +1,21 @@ +import 'dart:async'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; + +class SlideshowAction extends ActionBuilder { + const SlideshowAction(); + + @override + ActionItem create(BuildContext context, WidgetRef ref) => .new( + icon: Icons.slideshow, + label: context.t.slideshow, + onAction: () async => + unawaited(context.pushRoute(DriftSlideshowRoute(timeline: ref.read(timelineServiceProvider)))), + ); +} diff --git a/mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart deleted file mode 100644 index 9465f50500..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/cast_action_button.widget.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart'; - -class CastActionButton extends ConsumerWidget { - const CastActionButton({super.key, this.iconOnly = false, this.menuItem = false}); - - final bool iconOnly; - final bool menuItem; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); - - return BaseActionButton( - iconData: isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded, - iconColor: isCasting ? context.primaryColor : null, // null = default color - label: "cast".t(context: context), - onPressed: () { - unawaited(showDialog(context: context, builder: (context) => const CastDialog())); - }, - iconOnly: iconOnly, - menuItem: menuItem, - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/slideshow_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/slideshow_action_button.widget.dart deleted file mode 100644 index fdbc7a8cda..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/slideshow_action_button.widget.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; - -class SlideshowActionButton extends ConsumerWidget { - final bool iconOnly; - final bool menuItem; - - const SlideshowActionButton({super.key, this.iconOnly = false, this.menuItem = false}); - - Future _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - await context.pushRoute(DriftSlideshowRoute(timeline: ref.read(timelineServiceProvider))); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.slideshow, - label: "slideshow".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => unawaited(_onTap(context, ref)), - maxWidth: 100, - ); - } -} diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 32aaf360d0..41015fb064 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -10,22 +10,22 @@ import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; +import 'package:immich_mobile/presentation/actions/cast.action.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/actions/open_in_browser.action.dart'; import 'package:immich_mobile/presentation/actions/restore.action.dart'; import 'package:immich_mobile/presentation/actions/set_profile_picture.action.dart'; import 'package:immich_mobile/presentation/actions/similar_photos.action.dart'; +import 'package:immich_mobile/presentation/actions/slideshow.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/slideshow_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -179,7 +179,7 @@ enum ActionButtonType { iconOnly: iconOnly, menuItem: menuItem, ), - ActionButtonType.slideshow => SlideshowActionButton(iconOnly: iconOnly, menuItem: menuItem), + ActionButtonType.slideshow => const ActionMenuItem(action: SlideshowAction()), ActionButtonType.archive || ActionButtonType.unarchive => ActionMenuItem(action: ArchiveAction(source: context.source)), ActionButtonType.download => DownloadActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), @@ -228,7 +228,7 @@ enum ActionButtonType { EventStream.shared.emit(ScrollToDateEvent(context.asset.createdAt)); }, ), - ActionButtonType.cast => CastActionButton(iconOnly: iconOnly, menuItem: menuItem), + ActionButtonType.cast => const ActionMenuItem(action: CastAction()), }; } diff --git a/mobile/test/unit/presentation/actions/cast_action_test.dart b/mobile/test/unit/presentation/actions/cast_action_test.dart new file mode 100644 index 0000000000..45f1a842f5 --- /dev/null +++ b/mobile/test/unit/presentation/actions/cast_action_test.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/cast.action.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + + setUp(() async { + context = await PresentationContext.create(); + }); + + tearDown(() { + context.dispose(); + }); + + void Function(bool) captureConnectionListener() => + verify(() => context.service.cast.onConnectionState = captureAny()).captured.single as void Function(bool); + + group('CastAction', () { + testWidgets('offers to cast when nothing is connected', (tester) async { + await tester.pumpTestWidget(context, const ActionIconButton(action: CastAction())); + + expect(find.byIcon(Icons.cast_rounded), findsOneWidget); + }); + + testWidgets('switches to the connected icon once casting starts', (tester) async { + await tester.pumpTestWidget(context, const ActionIconButton(action: CastAction())); + + captureConnectionListener()(true); + await tester.pump(); + + expect(find.byIcon(Icons.cast_connected_rounded), findsOneWidget); + expect(find.byIcon(Icons.cast_rounded), findsNothing); + }); + + testWidgets('switches back when casting stops', (tester) async { + await tester.pumpTestWidget(context, const ActionIconButton(action: CastAction())); + + final onConnectionState = captureConnectionListener(); + onConnectionState(true); + await tester.pump(); + onConnectionState(false); + await tester.pump(); + + expect(find.byIcon(Icons.cast_rounded), findsOneWidget); + }); + }); +} From 9fcbb6eefc84dd9227e2d5f9abb3b5b1efa222c3 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Sat, 1 Aug 2026 08:06:42 +0530 Subject: [PATCH 38/81] refactor: mobile edit asset actions (#30264) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../actions/asset_debug.action.dart | 7 +- .../actions/edit_asset.action.dart | 84 +++++++++ .../actions/edit_datetime.action.dart | 100 ++++++++++ .../actions/edit_location.action.dart | 85 +++++++++ .../edit_date_time_action_button.widget.dart | 52 ------ .../edit_image_action_button.widget.dart | 59 ------ .../edit_location_action_button.widget.dart | 48 ----- .../date_time_details.widget.dart | 8 +- .../location_details.widget.dart | 14 +- .../asset_viewer/bottom_bar.widget.dart | 9 +- .../archive_bottom_sheet.widget.dart | 8 +- .../favorite_bottom_sheet.widget.dart | 8 +- .../general_bottom_sheet.widget.dart | 8 +- .../remote_album_bottom_sheet.widget.dart | 8 +- .../infrastructure/action.provider.dart | 81 --------- mobile/lib/services/action.service.dart | 98 ---------- .../infrastructure/action_provider_test.dart | 112 ------------ mobile/test/riverpod_mocks.dart | 8 + mobile/test/services/action.service_test.dart | 42 ----- .../actions/edit_action_test.dart | 172 ++++++++++++++++++ .../presentation/presentation_context.dart | 1 + 21 files changed, 481 insertions(+), 531 deletions(-) create mode 100644 mobile/lib/presentation/actions/edit_asset.action.dart create mode 100644 mobile/lib/presentation/actions/edit_datetime.action.dart create mode 100644 mobile/lib/presentation/actions/edit_location.action.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/edit_location_action_button.widget.dart delete mode 100644 mobile/test/providers/infrastructure/action_provider_test.dart create mode 100644 mobile/test/riverpod_mocks.dart create mode 100644 mobile/test/unit/presentation/actions/edit_action_test.dart diff --git a/mobile/lib/presentation/actions/asset_debug.action.dart b/mobile/lib/presentation/actions/asset_debug.action.dart index ff2935fc96..16ad15700e 100644 --- a/mobile/lib/presentation/actions/asset_debug.action.dart +++ b/mobile/lib/presentation/actions/asset_debug.action.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/generated/translations.g.dart'; @@ -13,16 +14,16 @@ class AssetDebugAction extends AssetActionBuilder { @override ActionItem? create(BuildContext context, WidgetRef ref) { - final assets = ref.watch(assetsActionProvider(source)).assets; + final asset = ref.watch(assetsActionProvider(source)).assets.singleOrNull; final troubleshootEnabled = ref.watch(settingsProvider.notifier).get(.advancedTroubleshooting); - if (!troubleshootEnabled || assets.length != 1) { + if (!troubleshootEnabled || asset == null) { return null; } return .new( icon: Icons.help_outline_rounded, label: context.t.troubleshoot, - onAction: () => unawaited(context.pushRoute(AssetTroubleshootRoute(asset: assets.single))), + onAction: () => unawaited(context.pushRoute(AssetTroubleshootRoute(asset: asset))), ); } } diff --git a/mobile/lib/presentation/actions/edit_asset.action.dart b/mobile/lib/presentation/actions/edit_asset.action.dart new file mode 100644 index 0000000000..d0a7e73122 --- /dev/null +++ b/mobile/lib/presentation/actions/edit_asset.action.dart @@ -0,0 +1,84 @@ +import 'dart:async'; + +import 'package:auto_route/auto_route.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.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/asset_edit.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/presentation/pages/edit/editor.provider.dart'; +import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/providers/websocket.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; +import 'package:immich_mobile/utils/semver.dart'; + +const _minimumServerVersion = SemVer(major: 2, minor: 6, patch: 0); + +final _stateProvider = Provider.family.autoDispose((ref, source) { + final isSupported = ref.watch(serverInfoProvider.select((state) => state.serverVersion >= _minimumServerVersion)); + if (!isSupported) { + return null; + } + + final assets = ref.watch(ownedAssetsActionProvider(source)); + return assets.where((asset) => asset.isEditable).singleOrNull; +}); + +class EditAssetAction extends AssetActionBuilder { + const EditAssetAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + if (!ref.watch(_stateProvider(source).select((asset) => asset != null))) { + return null; + } + + return .new(icon: Icons.tune, label: context.t.edit, onAction: () => _edit(context, ref)); + } + + Future _edit(BuildContext context, WidgetRef ref) async { + final asset = ref.read(_stateProvider(source)); + if (asset == null) { + return; + } + + try { + // TODO(shenlong): Move all EXIF and Apply Edits logic onto the Route + final repository = ref.read(remoteAssetRepositoryProvider); + final (edits, exif) = await (repository.getAssetEdits(asset.id), repository.getExif(asset.id)).wait; + if (exif == null || !context.mounted) { + return; + } + + ref.read(editorStateProvider.notifier).init(edits, exif); + unawaited( + context.pushRoute( + DriftEditImageRoute( + image: Image(image: getFullImageProvider(asset, edited: false)), + applyEdits: (newEdits) => applyEdits(ref, asset.id, newEdits), + ), + ), + ); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to open the editor for the asset"); + } + } +} + +@visibleForTesting +Future applyEdits(WidgetRef ref, String remoteId, List edits) async { + final websocket = ref.read(websocketProvider.notifier); + + bool isCurrentId(dynamic data) => data is Map && (data['asset'] as Map?)?['id'] == remoteId; + await ref.read(assetServiceProvider).applyEdits(remoteId, edits); + await Future.any([ + websocket.waitForEvent('AssetEditReadyV1', isCurrentId, const .new(seconds: 10)), + websocket.waitForEvent('AssetEditReadyV2', isCurrentId, const .new(seconds: 10)), + ]).catchError((_) {}); +} diff --git a/mobile/lib/presentation/actions/edit_datetime.action.dart b/mobile/lib/presentation/actions/edit_datetime.action.dart new file mode 100644 index 0000000000..a3c825c4db --- /dev/null +++ b/mobile/lib/presentation/actions/edit_datetime.action.dart @@ -0,0 +1,100 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.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/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; +import 'package:immich_mobile/utils/timezone.dart'; +import 'package:immich_mobile/widgets/common/date_time_picker.dart'; + +typedef _State = ({List assetIds, RemoteAsset? origin}); + +final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, source) { + final assets = ref.watch(ownedAssetsActionProvider(source)); + if (assets.isEmpty) { + return null; + } + + return (assetIds: assets.map((asset) => asset.id).toList(growable: false), origin: assets.singleOrNull); +}); + +class EditDateTimeAction extends AssetActionBuilder { + const EditDateTimeAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + if (!ref.watch(_stateProvider(source).select((state) => state != null))) { + return null; + } + + return .new( + icon: Icons.edit_calendar_outlined, + label: context.t.control_bottom_app_bar_edit_time, + onAction: () => _edit(context, ref), + ); + } + + Future _edit(BuildContext context, WidgetRef ref) async { + final state = ref.read(_stateProvider(source)); + if (state == null) { + return; + } + + final (:assetIds, :origin) = state; + final remoteAssetRepository = ref.read(remoteAssetRepositoryProvider); + final clearSelection = ref.read(clearSelectionProvider(source)); + + try { + DateTime? initialDate; + String? timeZone; + Duration? offset; + + if (origin != null) { + final exif = await remoteAssetRepository.getExif(origin.id); + + // Prefer the EXIF timezone, so the picker opens on what the asset actually shows. + DateTime dateTime = origin.createdAt.toLocal(); + offset = dateTime.timeZoneOffset; + if (exif?.dateTimeOriginal case final original?) { + timeZone = exif!.timeZone; + (dateTime, offset) = applyTimezoneOffset(dateTime: original, timeZone: exif.timeZone); + } + initialDate = dateTime; + + if (!context.mounted) { + return; + } + } + + final picked = await showDateTimePicker( + context: context, + initialDateTime: initialDate, + initialTZ: timeZone, + initialTZOffset: offset, + ); + if (picked == null || !context.mounted) { + return; + } + + await saveDateTime(context, ref, assetIds, picked); + clearSelection(); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to update the date and time for assets"); + } + } +} + +@visibleForTesting +Future saveDateTime(BuildContext context, WidgetRef ref, List assetIds, String dateTime) async { + final message = context.t.edit_date_and_time_action_prompt(count: assetIds.length); + final toastService = ref.read(toastServiceProvider); + + await ref.read(assetServiceProvider).update(assetIds, dateTime: .some(dateTime)); + ref.invalidate(assetExifProvider); + toastService.success(message); +} diff --git a/mobile/lib/presentation/actions/edit_location.action.dart b/mobile/lib/presentation/actions/edit_location.action.dart new file mode 100644 index 0000000000..f83a98099c --- /dev/null +++ b/mobile/lib/presentation/actions/edit_location.action.dart @@ -0,0 +1,85 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.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/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; +import 'package:immich_mobile/widgets/common/location_picker.dart'; +import 'package:maplibre_gl/maplibre_gl.dart'; + +typedef _State = ({List assetIds, RemoteAsset? origin}); + +final _stateProvider = Provider.family.autoDispose<_State?, ActionSource>((ref, source) { + final assets = ref.watch(ownedAssetsActionProvider(source)); + if (assets.isEmpty) { + return null; + } + + return (assetIds: assets.map((asset) => asset.id).toList(growable: false), origin: assets.singleOrNull); +}); + +class EditLocationAction extends AssetActionBuilder { + const EditLocationAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + if (!ref.watch(_stateProvider(source).select((state) => state != null))) { + return null; + } + + return .new( + icon: Icons.edit_location_alt_outlined, + label: context.t.control_bottom_app_bar_edit_location, + onAction: () => _edit(context, ref), + ); + } + + Future _edit(BuildContext context, WidgetRef ref) async { + final state = ref.read(_stateProvider(source)); + if (state == null) { + return; + } + + final (:assetIds, :origin) = state; + final remoteAssetRepository = ref.read(remoteAssetRepositoryProvider); + final clearSelection = ref.read(clearSelectionProvider(source)); + + try { + LatLng? initialLatLng; + if (origin != null) { + final exif = await remoteAssetRepository.getExif(origin.id); + if (exif?.latitude != null && exif?.longitude != null) { + initialLatLng = LatLng(exif!.latitude!, exif.longitude!); + } + if (!context.mounted) { + return; + } + } + + final location = await showLocationPicker(context: context, initialLatLng: initialLatLng); + if (location == null || !context.mounted) { + return; + } + + await saveLocation(context, ref, assetIds, location); + clearSelection(); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to update the location for assets"); + } + } +} + +@visibleForTesting +Future saveLocation(BuildContext context, WidgetRef ref, List assetIds, LatLng location) async { + final message = context.t.edit_location_action_prompt(count: assetIds.length); + final toastService = ref.read(toastServiceProvider); + + await ref.read(assetServiceProvider).update(assetIds, location: .some(location)); + ref.invalidate(assetExifProvider); + toastService.success(message); +} diff --git a/mobile/lib/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart deleted file mode 100644 index e93720186b..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class EditDateTimeActionButton extends ConsumerWidget { - final ActionSource source; - - const EditDateTimeActionButton({super.key, required this.source}); - - Future _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final result = await ref.read(actionProvider.notifier).editDateTime(source, context); - if (result == null) { - return; - } - - ref.read(multiSelectProvider.notifier).reset(); - if (!context.mounted) { - return; - } - - final successMessage = 'edit_date_and_time_action_prompt'.t( - context: context, - args: {'count': result.count.toString()}, - ); - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - maxWidth: 95.0, - iconData: Icons.edit_calendar_outlined, - label: "control_bottom_app_bar_edit_time".t(context: context), - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart deleted file mode 100644 index 564b02d884..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/edit_image_action_button.widget.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'dart:async'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/models/asset_edit.model.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/pages/edit/editor.provider.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; - -class EditImageActionButton extends ConsumerWidget { - const EditImageActionButton({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final currentAsset = ref.watch(assetViewerProvider.select((s) => s.currentAsset)); - - Future editImage(List edits) async { - if (currentAsset == null || currentAsset.remoteId == null) { - return; - } - - await ref.read(actionProvider.notifier).applyEdits(ActionSource.viewer, edits); - } - - Future onPress() async { - if (currentAsset == null || currentAsset.remoteId == null) { - return; - } - - final imageProvider = getFullImageProvider(currentAsset, edited: false); - - final image = Image(image: imageProvider); - final (edits, exifInfo) = await ( - ref.read(remoteAssetRepositoryProvider).getAssetEdits(currentAsset.remoteId!), - ref.read(remoteAssetRepositoryProvider).getExif(currentAsset.remoteId!), - ).wait; - - if (exifInfo == null) { - return; - } - - ref.read(editorStateProvider.notifier).init(edits, exifInfo); - await context.pushRoute(DriftEditImageRoute(image: image, applyEdits: editImage)); - } - - return BaseActionButton( - iconData: Icons.tune, - label: "edit".t(context: context), - onPressed: onPress, - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/edit_location_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/edit_location_action_button.widget.dart deleted file mode 100644 index b250e325ce..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/edit_location_action_button.widget.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class EditLocationActionButton extends ConsumerWidget { - final ActionSource source; - - const EditLocationActionButton({super.key, required this.source}); - - Future _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final result = await ref.read(actionProvider.notifier).editLocation(source, context); - if (result == null) { - return; - } - - ref.read(multiSelectProvider.notifier).reset(); - if (!context.mounted) { - return; - } - - final successMessage = 'edit_location_action_prompt'.t(context: context, args: {'count': result.count.toString()}); - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.edit_location_alt_outlined, - label: "control_bottom_app_bar_edit_location".t(context: context), - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/date_time_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/date_time_details.widget.dart index 2dc1c40456..eff5705eae 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/date_time_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/date_time_details.widget.dart @@ -9,6 +9,7 @@ import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/duration_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -28,16 +29,15 @@ class DateTimeDetails extends ConsumerWidget { final asset = this.asset; final exifInfo = this.exifInfo; final isOwner = ref.watch(currentUserProvider)?.id == (asset is RemoteAsset ? asset.ownerId : null); + final editDateTime = const EditDateTimeAction(source: .viewer).create(context, ref); return Column( children: [ SheetTile( title: _getDateTime(context, asset, exifInfo), titleStyle: context.textTheme.labelLarge, - trailing: asset.hasRemote && isOwner ? const Icon(Icons.edit, size: 18) : null, - onTap: asset.hasRemote && isOwner - ? () async => await ref.read(actionProvider.notifier).editDateTime(ActionSource.viewer, context) - : null, + trailing: editDateTime == null ? null : const Icon(Icons.edit, size: 18), + onTap: editDateTime?.onAction, ), if (exifInfo != null) _SheetAssetDescription(exif: exifInfo, isEditable: isOwner), ], diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart index 8edfca5bf1..c6d048e458 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_details/location_details.widget.dart @@ -2,14 +2,13 @@ import 'dart:async'; import 'package:flutter/material.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/exif.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/actions/edit_location.action.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -55,10 +54,6 @@ class _LocationDetailsState extends ConsumerState { } } - Future editLocation() async { - await ref.read(actionProvider.notifier).editLocation(ActionSource.viewer, context); - } - @override Widget build(BuildContext context) { final asset = widget.asset; @@ -70,6 +65,7 @@ class _LocationDetailsState extends ConsumerState { return const SizedBox.shrink(); } + final editLocation = const EditLocationAction(source: .viewer).create(context, ref); final locationName = _getLocationName(exifInfo); final coordinates = "${exifInfo?.latitude?.toStringAsFixed(4)}, ${exifInfo?.longitude?.toStringAsFixed(4)}"; @@ -81,8 +77,8 @@ class _LocationDetailsState extends ConsumerState { SheetTile( title: 'location'.t(context: context), titleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceSecondary), - trailing: hasCoordinates ? const Icon(Icons.edit_location_alt, size: 20) : null, - onTap: editLocation, + trailing: hasCoordinates && editLocation != null ? const Icon(Icons.edit_location_alt, size: 20) : null, + onTap: editLocation?.onAction, ), if (hasCoordinates) Padding( @@ -117,7 +113,7 @@ class _LocationDetailsState extends ConsumerState { color: context.primaryColor, ), leading: const Icon(Icons.location_off), - onTap: editLocation, + onTap: editLocation?.onAction, ), ], ), diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart index 33eb917c69..e86088cb2d 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -5,9 +5,9 @@ import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/presentation/actions/action.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; +import 'package:immich_mobile/presentation/actions/edit_asset.action.dart'; import 'package:immich_mobile/presentation/actions/restore.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/ocr_toggle_button.widget.dart'; @@ -15,8 +15,6 @@ import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart' import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; -import 'package:immich_mobile/utils/semver.dart'; import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart'; import 'package:immich_ui/immich_ui.dart'; @@ -41,7 +39,6 @@ class ViewerBottomBar extends ConsumerWidget { final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); final showingDetails = ref.watch(assetViewerProvider.select((s) => s.showingDetails)); final isInLockedView = ref.watch(inLockedViewProvider); - final serverInfo = ref.watch(serverInfoProvider); final isInTrash = ref.read(timelineServiceProvider).origin == TimelineOrigin.trash; final originalTheme = context.themeData; @@ -53,9 +50,7 @@ class ViewerBottomBar extends ConsumerWidget { if (!isInLockedView) ...[ if (!isInTrash) ...[ if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer), - // edit sync was added in 2.6.0 - if (asset.isEditable && serverInfo.serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)) - const EditImageActionButton(), + ..._actionColumnButtons(context, ref, const [EditAssetAction(source: .viewer)]), if (asset.hasRemote) AddActionButton(originalTheme: originalTheme), ], ..._actionColumnButtons(context, ref, const [DeleteAction(source: .viewer)]), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart index c8e62c8ec1..9290e157b3 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart @@ -6,12 +6,12 @@ import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; +import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart'; +import 'package:immich_mobile/presentation/actions/edit_location.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; @@ -83,8 +83,8 @@ class _ArchiveBottomSheetState extends ConsumerState { const ActionColumnButton(action: FavoriteAction(source: .timeline)), if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline), const ActionColumnButton(action: DeleteAction(source: .timeline)), - const EditDateTimeActionButton(source: ActionSource.timeline), - const EditLocationActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: EditDateTimeAction(source: .timeline)), + const ActionColumnButton(action: EditLocationAction(source: .timeline)), const ActionColumnButton(action: LockAction(source: .timeline)), const ActionColumnButton(action: StackAction(source: .timeline)), ], diff --git a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart index ece09d5a76..ac0dd14922 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart @@ -7,12 +7,12 @@ import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; +import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart'; +import 'package:immich_mobile/presentation/actions/edit_location.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; @@ -83,8 +83,8 @@ class FavoriteBottomSheet extends ConsumerWidget { const ActionColumnButton(action: ArchiveAction(source: .timeline)), if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline), const ActionColumnButton(action: DeleteAction(source: .timeline)), - const EditDateTimeActionButton(source: ActionSource.timeline), - const EditLocationActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: EditDateTimeAction(source: .timeline)), + const ActionColumnButton(action: EditLocationAction(source: .timeline)), const ActionColumnButton(action: LockAction(source: .timeline)), const ActionColumnButton(action: StackAction(source: .timeline)), ], diff --git a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart index c2bf3edf96..0ca4af56ce 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart @@ -6,13 +6,13 @@ import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; +import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart'; +import 'package:immich_mobile/presentation/actions/edit_location.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; @@ -90,8 +90,8 @@ class _GeneralBottomSheetState extends ConsumerState { const ActionColumnButton(action: FavoriteAction(source: .timeline)), const ActionColumnButton(action: ArchiveAction(source: .timeline)), if (tagsEnabled) const BulkTagAssetsActionButton(source: ActionSource.timeline), - const EditDateTimeActionButton(source: ActionSource.timeline), - const EditLocationActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: EditDateTimeAction(source: .timeline)), + const ActionColumnButton(action: EditLocationAction(source: .timeline)), const ActionColumnButton(action: LockAction(source: .timeline)), const ActionColumnButton(action: StackAction(source: .timeline)), ], diff --git a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart index a9175577b0..f273a70dfe 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart @@ -6,12 +6,12 @@ import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; +import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart'; +import 'package:immich_mobile/presentation/actions/edit_location.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; @@ -97,8 +97,8 @@ class _RemoteAlbumBottomSheetState extends ConsumerState const DownloadActionButton(source: ActionSource.timeline), if (ownsAlbum) ...[ const ActionColumnButton(action: DeleteAction(source: .timeline)), - const EditDateTimeActionButton(source: ActionSource.timeline), - const EditLocationActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: EditDateTimeAction(source: .timeline)), + const ActionColumnButton(action: EditLocationAction(source: .timeline)), const ActionColumnButton(action: LockAction(source: .timeline)), const ActionColumnButton(action: StackAction(source: .timeline)), ], diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index 2222cbf722..d9ca0df303 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -6,23 +6,17 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/asset_edit.model.dart'; import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart' show assetExifProvider; import 'package:immich_mobile/providers/infrastructure/tag.provider.dart'; -import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/action.service.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart'; -import 'package:immich_mobile/utils/semver.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; final actionProvider = NotifierProvider(ActionNotifier.new, dependencies: [multiSelectProvider]); @@ -119,50 +113,6 @@ class ActionNotifier extends Notifier { } } - Future editLocation(ActionSource source, BuildContext context) async { - final ids = _getOwnedRemoteIdsForSource(source); - try { - final isEdited = await _service.editLocation(ids, context); - if (!isEdited) { - return null; - } - - // This must be called since editing location - // does not update the currentAsset which means - // the exif provider will not be refreshed automatically - if (source == ActionSource.viewer) { - final currentAsset = ref.read(assetViewerProvider).currentAsset; - if (currentAsset != null) { - ref.invalidate(assetExifProvider(currentAsset)); - } - } - - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to edit location for assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - - Future editDateTime(ActionSource source, BuildContext context) async { - final ids = _getOwnedRemoteIdsForSource(source); - try { - final isEdited = await _service.editDateTime(ids, context); - if (!isEdited) { - return null; - } - - if (source == ActionSource.viewer) { - ref.invalidate(assetExifProvider); - } - - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to edit date and time for assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - Future tagAssets(ActionSource source, BuildContext context) async { final ids = _getOwnedRemoteIdsForSource(source); try { @@ -405,37 +355,6 @@ class ActionNotifier extends Notifier { }); } } - - Future applyEdits(ActionSource source, List edits) async { - final ids = _getOwnedRemoteIdsForSource(source); - - if (ids.length != 1) { - _logger.warning('applyEdits called with multiple assets, expected single asset'); - return ActionResult(count: ids.length, success: false, error: 'Expected single asset for applying edits'); - } - - Future editReady; - if (ref.read(serverInfoProvider).serverVersion >= const SemVer(major: 3, minor: 0, patch: 0)) { - editReady = ref.read(websocketProvider.notifier).waitForEvent("AssetEditReadyV2", (dynamic data) { - final eventAsset = SyncAssetV2.fromJson(data["asset"]); - return eventAsset?.id == ids.first; - }, const Duration(seconds: 10)); - } else { - editReady = ref.read(websocketProvider.notifier).waitForEvent("AssetEditReadyV1", (dynamic data) { - final eventAsset = SyncAssetV1.fromJson(data["asset"]); - return eventAsset?.id == ids.first; - }, const Duration(seconds: 10)); - } - - try { - await _service.applyEdits(ids.first, edits); - await editReady; - return const ActionResult(count: 1, success: true); - } catch (error, stack) { - _logger.severe('Failed to apply edits to assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } } extension on Iterable { diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index 28e2d27c7e..415a9c578b 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.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/asset_edit.model.dart'; import 'package:immich_mobile/domain/services/tag.service.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; @@ -16,11 +15,7 @@ import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/repositories/download.repository.dart'; import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/utils/timezone.dart'; -import 'package:immich_mobile/widgets/common/date_time_picker.dart'; -import 'package:immich_mobile/widgets/common/location_picker.dart'; import 'package:immich_mobile/widgets/common/tag_picker.dart'; -import 'package:maplibre_gl/maplibre_gl.dart' as maplibre; final actionServiceProvider = Provider( (ref) => ActionService( @@ -69,91 +64,6 @@ class ActionService { return count; } - Future editLocation(List remoteIds, BuildContext context) async { - maplibre.LatLng? initialLatLng; - if (remoteIds.length == 1) { - final exif = await _remoteAssetRepository.getExif(remoteIds[0]); - - if (exif?.latitude != null && exif?.longitude != null) { - initialLatLng = maplibre.LatLng(exif!.latitude!, exif.longitude!); - } - } - - if (!context.mounted) { - return false; - } - - final location = await showLocationPicker(context: context, initialLatLng: initialLatLng); - - if (location == null) { - return false; - } - - await _assetApiRepository.updateLocation(remoteIds, location); - await _remoteAssetRepository.updateLocation(remoteIds, location); - - return true; - } - - Future editDateTime(List remoteIds, BuildContext context) async { - DateTime? initialDate; - String? timeZone; - Duration? offset; - - if (remoteIds.length == 1) { - final assetId = remoteIds.first; - final asset = await _remoteAssetRepository.get(assetId); - if (asset == null) { - return false; - } - - final exifData = await _remoteAssetRepository.getExif(assetId); - - // Use EXIF timezone information if available (matching web app and display behavior) - DateTime dt = asset.createdAt.toLocal(); - offset = dt.timeZoneOffset; - - if (exifData?.dateTimeOriginal != null) { - timeZone = exifData!.timeZone; - (dt, offset) = applyTimezoneOffset(dateTime: exifData.dateTimeOriginal!, timeZone: exifData.timeZone); - } - - initialDate = dt; - } - - if (!context.mounted) { - return false; - } - - final dateTime = await showDateTimePicker( - context: context, - initialDateTime: initialDate, - initialTZ: timeZone, - initialTZOffset: offset, - ); - - if (dateTime == null) { - return false; - } - - await applyDateTime(remoteIds, dateTime); - - return true; - } - - @visibleForTesting - Future applyDateTime(List remoteIds, String dateTime) async { - final parsedDateTime = DateTime.parse(dateTime); - final offset = RegExp(r'[+-]\d{2}:\d{2}$').firstMatch(dateTime)?.group(0); - - await _assetApiRepository.updateDateTime(remoteIds, dateTime); - await _remoteAssetRepository.updateDateTime( - remoteIds, - parsedDateTime, - timeZone: offset == null ? null : 'UTC$offset', - ); - } - Future removeFromAlbum(List remoteIds, String albumId) async { final result = await _albumApiRepository.removeAssets(albumId, remoteIds); if (result.removed.isNotEmpty) { @@ -224,12 +134,4 @@ class ActionService { await _remoteAlbumRepository.update(updatedAlbum); return true; } - - Future applyEdits(String remoteId, List edits) async { - if (edits.isEmpty) { - await _assetApiRepository.removeEdits(remoteId); - } else { - await _assetApiRepository.editAsset(remoteId, edits); - } - } } diff --git a/mobile/test/providers/infrastructure/action_provider_test.dart b/mobile/test/providers/infrastructure/action_provider_test.dart deleted file mode 100644 index 7f5c8d4ec2..0000000000 --- a/mobile/test/providers/infrastructure/action_provider_test.dart +++ /dev/null @@ -1,112 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.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/user.model.dart'; -import 'package:immich_mobile/domain/services/asset.service.dart'; -import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/services/action.service.dart'; -import 'package:immich_mobile/services/foreground_upload.service.dart'; -import 'package:mocktail/mocktail.dart'; - -class MockActionService extends Mock implements ActionService {} - -class MockAssetService extends Mock implements AssetService {} - -class MockForegroundUploadService extends Mock implements ForegroundUploadService {} - -class MockUserService extends Mock implements UserService {} - -class FakeBuildContext extends Fake implements BuildContext {} - -final _user = UserDto(id: 'user-1', email: 'user@test.dev', name: 'user', profileChangedAt: DateTime(2026)); - -final _asset = RemoteAsset( - id: 'asset-1', - name: 'photo.jpg', - ownerId: 'user-1', - checksum: 'checksum-1', - type: AssetType.image, - createdAt: DateTime(2026, 6, 10, 10, 27), - updatedAt: DateTime(2026, 6, 10, 10, 27), - isEdited: false, -); - -void main() { - late ProviderContainer container; - late MockActionService actionService; - late MockAssetService assetService; - - setUpAll(() { - registerFallbackValue(FakeBuildContext()); - registerFallbackValue(_asset); - registerFallbackValue([]); - }); - - setUp(() { - actionService = MockActionService(); - assetService = MockAssetService(); - final userService = MockUserService(); - - when(() => actionService.editDateTime(any(), any())).thenAnswer((_) async => true); - when(() => assetService.watchAsset(any())).thenAnswer((_) => const Stream.empty()); - when(() => assetService.getExif(any())).thenAnswer((_) async => null); - when(() => userService.tryGetMyUser()).thenReturn(_user); - when(() => userService.watchMyUser()).thenAnswer((_) => const Stream.empty()); - - container = ProviderContainer( - overrides: [ - actionServiceProvider.overrideWithValue(actionService), - assetServiceProvider.overrideWithValue(assetService), - foregroundUploadServiceProvider.overrideWithValue(MockForegroundUploadService()), - currentUserProvider.overrideWith((ref) => CurrentUserProvider(userService)), - ], - ); - addTearDown(container.dispose); - }); - - group('editDateTime', () { - test('refreshes the exif provider when editing from the viewer', () async { - container.read(assetViewerProvider.notifier).setAsset(_asset); - container.listen(assetExifProvider(_asset), (_, __) {}); - await container.read(assetExifProvider(_asset).future); - - final result = await container.read(actionProvider.notifier).editDateTime(ActionSource.viewer, FakeBuildContext()); - - expect(result?.success, isTrue); - await container.read(assetExifProvider(_asset).future); - verify(() => assetService.getExif(_asset)).called(2); - }); - - test('leaves the exif provider cached when editing from the timeline', () async { - container.read(assetViewerProvider.notifier).setAsset(_asset); - container.listen(assetExifProvider(_asset), (_, __) {}); - await container.read(assetExifProvider(_asset).future); - - final result = await container.read(actionProvider.notifier).editDateTime(ActionSource.timeline, FakeBuildContext()); - - expect(result?.success, isTrue); - await container.read(assetExifProvider(_asset).future); - verify(() => assetService.getExif(_asset)).called(1); - }); - - test('does not refresh the exif provider when the edit is cancelled', () async { - when(() => actionService.editDateTime(any(), any())).thenAnswer((_) async => false); - container.read(assetViewerProvider.notifier).setAsset(_asset); - container.listen(assetExifProvider(_asset), (_, __) {}); - await container.read(assetExifProvider(_asset).future); - - final result = await container.read(actionProvider.notifier).editDateTime(ActionSource.viewer, FakeBuildContext()); - - expect(result, isNull); - await container.read(assetExifProvider(_asset).future); - verify(() => assetService.getExif(_asset)).called(1); - }); - }); -} diff --git a/mobile/test/riverpod_mocks.dart b/mobile/test/riverpod_mocks.dart new file mode 100644 index 0000000000..24867b226e --- /dev/null +++ b/mobile/test/riverpod_mocks.dart @@ -0,0 +1,8 @@ +import 'package:immich_mobile/models/server_info/server_version.model.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; + +class StubServerInfoNotifier extends ServerInfoNotifier { + StubServerInfoNotifier(super.serverInfoService, {required ServerVersion version}) { + state = state.copyWith(serverVersion: version); + } +} diff --git a/mobile/test/services/action.service_test.dart b/mobile/test/services/action.service_test.dart index 76f02e29d0..0a1fbf08bc 100644 --- a/mobile/test/services/action.service_test.dart +++ b/mobile/test/services/action.service_test.dart @@ -89,46 +89,4 @@ void main() { }); }); - group('ActionService.applyDateTime', () { - const ids = ['asset_id_1']; - - test('sends the picked value to the api with its offset intact', () async { - const picked = '2026-06-10T19:15:00.000+06:00'; - when(() => assetApiRepository.updateDateTime(ids, picked)).thenAnswer((_) async {}); - when( - () => remoteAssetRepository.updateDateTime(ids, DateTime.parse(picked), timeZone: 'UTC+06:00'), - ).thenAnswer((_) async {}); - - await sut.applyDateTime(ids, picked); - - verify(() => assetApiRepository.updateDateTime(ids, picked)).called(1); - verify(() => remoteAssetRepository.updateDateTime(ids, DateTime.parse(picked), timeZone: 'UTC+06:00')).called(1); - }); - - test('handles negative offsets', () async { - const picked = '2026-01-05T08:00:00.000-05:30'; - when(() => assetApiRepository.updateDateTime(ids, picked)).thenAnswer((_) async {}); - when( - () => remoteAssetRepository.updateDateTime(ids, DateTime.parse(picked), timeZone: 'UTC-05:30'), - ).thenAnswer((_) async {}); - - await sut.applyDateTime(ids, picked); - - verify(() => assetApiRepository.updateDateTime(ids, picked)).called(1); - verify(() => remoteAssetRepository.updateDateTime(ids, DateTime.parse(picked), timeZone: 'UTC-05:30')).called(1); - }); - - test('writes no timezone when the value has no offset', () async { - const picked = '2026-06-10T13:15:00.000Z'; - when(() => assetApiRepository.updateDateTime(ids, picked)).thenAnswer((_) async {}); - when( - () => remoteAssetRepository.updateDateTime(ids, DateTime.parse(picked), timeZone: null), - ).thenAnswer((_) async {}); - - await sut.applyDateTime(ids, picked); - - verify(() => assetApiRepository.updateDateTime(ids, picked)).called(1); - verify(() => remoteAssetRepository.updateDateTime(ids, DateTime.parse(picked), timeZone: null)).called(1); - }); - }); } diff --git a/mobile/test/unit/presentation/actions/edit_action_test.dart b/mobile/test/unit/presentation/actions/edit_action_test.dart new file mode 100644 index 0000000000..063301f9c4 --- /dev/null +++ b/mobile/test/unit/presentation/actions/edit_action_test.dart @@ -0,0 +1,172 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/models/server_info/server_version.model.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/edit_asset.action.dart'; +import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart'; +import 'package:immich_mobile/presentation/actions/edit_location.action.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/utils/option.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../riverpod_mocks.dart'; +import '../../../service.mocks.dart'; +import '../../factories/remote_asset_factory.dart'; +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + late MockAssetService assetService; + + setUp(() async { + context = await PresentationContext.create(); + assetService = context.service.asset.service; + }); + + tearDown(() { + context.dispose(); + }); + + RemoteAsset owned({AssetType type = .image}) => + RemoteAssetFactory.create(ownerId: context.currentUser.id, type: type); + + const supportedVersion = ServerVersion(major: 2, minor: 6, patch: 0); + + List reportedVersion(ServerVersion version) => [ + serverInfoProvider.overrideWith((ref) => StubServerInfoNotifier(context.service.serverInfo, version: version)), + ]; + + late BuildContext actionContext; + late WidgetRef actionRef; + + Future pumpAction( + WidgetTester tester, + ActionBuilder action, + Set selection, { + List overrides = const [], + }) => tester.pumpTestWidget( + context, + Consumer( + builder: (widgetContext, ref, _) { + actionContext = widgetContext; + actionRef = ref; + return ActionIconButton(action: action); + }, + ), + overrides: [...context.selected(selection), ...overrides], + ); + + group('EditAssetAction', () { + Future pumpEditAsset( + WidgetTester tester, + Set selection, { + ServerVersion version = supportedVersion, + }) => pumpAction( + tester, + const EditAssetAction(source: .timeline), + selection, + overrides: [...reportedVersion(version)], + ); + + testWidgets('offers to edit a single owned editable asset', (tester) async { + await pumpEditAsset(tester, {owned()}); + + expect(find.byType(ImmichIconButton), findsOneWidget); + }); + + testWidgets('is hidden when the server predates edit sync', (tester) async { + await pumpEditAsset(tester, {owned()}, version: const ServerVersion(major: 2, minor: 5, patch: 9)); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('is hidden for more than one asset', (tester) async { + await pumpEditAsset(tester, {owned(), owned()}); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('is hidden for an asset owned by someone else', (tester) async { + await pumpEditAsset(tester, {RemoteAssetFactory.create()}); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('is hidden for a non-editable asset', (tester) async { + await pumpEditAsset(tester, {owned(type: .video)}); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('reads the edits and exif for the asset it is about to open', (tester) async { + final asset = owned(); + + await pumpEditAsset(tester, {asset}); + await tester.tap(find.byType(ImmichIconButton)); + await tester.pump(); + + verify(() => context.repository.remoteAsset.repo.getAssetEdits(asset.id)).called(1); + verify(() => context.repository.remoteAsset.repo.getExif(asset.id)).called(1); + }); + }); + + group('EditLocationAction', () { + testWidgets('offers to edit an owned remote asset', (tester) async { + await pumpAction(tester, const EditLocationAction(source: .timeline), {owned()}); + + expect(find.byType(ImmichIconButton), findsOneWidget); + }); + + testWidgets('is hidden without any owned remote asset', (tester) async { + await pumpAction(tester, const EditLocationAction(source: .timeline), {RemoteAssetFactory.create()}); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('saves the location against every owned asset and toasts the count', (tester) async { + final mine = owned(); + final theirs = RemoteAssetFactory.create(); + + await pumpAction(tester, const EditLocationAction(source: .timeline), {mine, theirs}); + await saveLocation(actionContext, actionRef, [mine.id], const LatLng(1, 2)); + await tester.pumpAndSettle(); + + final location = + verify(() => assetService.update([mine.id], location: captureAny(named: 'location'))).captured.single + as Option; + expect(location.unwrapOrNull?.latitude, 1); + expect(location.unwrapOrNull?.longitude, 2); + }); + }); + + group('EditDateTimeAction', () { + testWidgets('offers to edit an owned remote asset', (tester) async { + await pumpAction(tester, const EditDateTimeAction(source: .timeline), {owned()}); + + expect(find.byType(ImmichIconButton), findsOneWidget); + }); + + testWidgets('is hidden without any owned remote asset', (tester) async { + await pumpAction(tester, const EditDateTimeAction(source: .timeline), {RemoteAssetFactory.create()}); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('saves the date against every owned asset and toasts the count', (tester) async { + const picked = '2026-06-10T19:15:00.000+06:00'; + final mine = owned(); + final theirs = RemoteAssetFactory.create(); + + await pumpAction(tester, const EditDateTimeAction(source: .timeline), {mine, theirs}); + await saveDateTime(actionContext, actionRef, [mine.id], picked); + await tester.pumpAndSettle(); + + verify(() => assetService.update([mine.id], dateTime: const Some(picked))).called(1); + }); + }); +} diff --git a/mobile/test/unit/presentation/presentation_context.dart b/mobile/test/unit/presentation/presentation_context.dart index 2e5871a419..ba12183652 100644 --- a/mobile/test/unit/presentation/presentation_context.dart +++ b/mobile/test/unit/presentation/presentation_context.dart @@ -54,6 +54,7 @@ class PresentationContext { gCastServiceProvider.overrideWithValue(service.cast), serverInfoServiceProvider.overrideWithValue(service.serverInfo), inLockedViewProvider.overrideWithValue(false), + remoteAssetRepositoryProvider.overrideWithValue(repository.remoteAsset.repo), ]; List selected(Set assets) => [ From 0a97b4b099a891feb6582fcc768132542bd7be66 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Sat, 1 Aug 2026 08:06:42 +0530 Subject: [PATCH 39/81] refactor: mobile share actions (#29947) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../presentation/actions/share.action.dart | 164 +++++++++++++++ .../actions/share_link.action.dart | 33 +++ .../share_action_button.widget.dart | 192 ------------------ .../share_link_action_button.widget.dart | 33 --- .../asset_viewer/bottom_bar.widget.dart | 4 +- .../archive_bottom_sheet.widget.dart | 8 +- .../favorite_bottom_sheet.widget.dart | 8 +- .../general_bottom_sheet.widget.dart | 8 +- .../local_album_bottom_sheet.widget.dart | 4 +- .../locked_folder_bottom_sheet.widget.dart | 4 +- .../partner_detail_bottom_sheet.widget.dart | 5 +- .../remote_album_bottom_sheet.widget.dart | 8 +- .../infrastructure/action.provider.dart | 35 ---- mobile/lib/services/action.service.dart | 27 --- mobile/lib/utils/action_button.utils.dart | 12 +- mobile/test/services/action.service_test.dart | 4 - .../share_action_button_test.dart | 117 ----------- .../actions/share_action_test.dart | 159 +++++++++++++++ .../presentation/presentation_context.dart | 2 + 19 files changed, 387 insertions(+), 440 deletions(-) create mode 100644 mobile/lib/presentation/actions/share.action.dart create mode 100644 mobile/lib/presentation/actions/share_link.action.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/share_link_action_button.widget.dart delete mode 100644 mobile/test/unit/presentation/action_buttons/share_action_button_test.dart create mode 100644 mobile/test/unit/presentation/actions/share_action_test.dart diff --git a/mobile/lib/presentation/actions/share.action.dart b/mobile/lib/presentation/actions/share.action.dart new file mode 100644 index 0000000000..4c36493265 --- /dev/null +++ b/mobile/lib/presentation/actions/share.action.dart @@ -0,0 +1,164 @@ +import 'dart:async'; + +import 'package:flutter/material.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/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/platform_extensions.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/repositories/asset_media.repository.dart'; + +final _stateProvider = Provider.family.autoDispose?, ActionSource>((ref, source) { + final assets = ref.watch(assetsActionProvider(source)); + final shareable = assets.toList(growable: false); + return shareable.isEmpty ? null : shareable; +}); + +class ShareAction extends AssetActionBuilder { + const ShareAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + final assets = ref.watch(_stateProvider(source)); + if (assets == null) { + return null; + } + + return .new( + icon: CurrentPlatform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded, + label: context.t.share, + onAction: () => _share(context, ref, assets, ref.read(appConfigProvider).share.fileType), + onSecondaryAction: () => _promptQualityAndShare(context, ref, assets), + ); + } + + Future _promptQualityAndShare(BuildContext context, WidgetRef ref, List assets) async { + // Only show preview option when at least one of the assets is not a video + final showPreview = assets.any((asset) => !asset.isVideo); + + final fileType = await showDialog( + context: context, + builder: (_) => _ShareFileTypeDialog(showPreview: showPreview), + useRootNavigator: false, + ); + if (fileType == null || !context.mounted) { + return; + } + + await _share(context, ref, assets, fileType); + } + + Future _share(BuildContext context, WidgetRef ref, List assets, ShareAssetType fileType) async { + final cancelCompleter = Completer(); + final progress = ValueNotifier(null); + final mediaRepository = ref.read(assetMediaRepositoryProvider); + final toastService = ref.read(toastServiceProvider); + final errorMessage = context.t.scaffold_body_error_occurred; + + await showDialog( + context: context, + barrierDismissible: false, + useRootNavigator: false, + builder: (dialogContext) { + void finish({required bool failed}) { + if (cancelCompleter.isCompleted || !dialogContext.mounted) { + return; + } + if (failed) { + unawaited(.value(toastService.error(errorMessage))); + } + dialogContext.pop(); + } + + unawaited( + mediaRepository + .shareAssets( + assets, + context, + fileType: fileType, + cancelCompleter: cancelCompleter, + onAssetDownloadProgress: (value) => progress.value = value, + ) + .then((count) => finish(failed: count == 0), onError: (_) => finish(failed: true)), + ); + + return _SharePreparingDialog(progress: progress); + }, + ).then((_) { + if (!cancelCompleter.isCompleted) { + cancelCompleter.complete(); + } + progress.dispose(); + }); + } +} + +class _SharePreparingDialog extends StatelessWidget { + final ValueNotifier progress; + + const _SharePreparingDialog({required this.progress}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + content: Column( + mainAxisSize: .min, + children: [ + Container(margin: const .only(bottom: 12), child: Text(context.t.share_dialog_preparing)), + SizedBox( + width: 240, + child: ValueListenableBuilder( + valueListenable: progress, + builder: (context, value, _) { + final percent = value == null ? null : (value * 100).clamp(0, 100); + return Column( + mainAxisSize: .min, + children: [ + LinearProgressIndicator(value: value, minHeight: 8.0), + if (percent != null) + Container(margin: const .only(top: 8), child: Text('${percent.toStringAsFixed(0)}%')), + ], + ); + }, + ), + ), + ], + ), + ); + } +} + +class _ShareFileTypeDialog extends StatelessWidget { + final bool showPreview; + + const _ShareFileTypeDialog({this.showPreview = true}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(context.t.select_quality), + contentPadding: const .symmetric(vertical: 8), + content: Column( + mainAxisSize: .min, + children: [ + ListTile( + leading: const Icon(Icons.high_quality_rounded), + title: Text(context.t.share_original), + onTap: () => context.pop(ShareAssetType.original), + ), + if (showPreview) + ListTile( + leading: const Icon(Icons.photo_size_select_large_rounded), + title: Text(context.t.share_preview), + onTap: () => context.pop(ShareAssetType.preview), + ), + ], + ), + actions: [TextButton(onPressed: () => context.pop(), child: Text(context.t.cancel))], + ); + } +} diff --git a/mobile/lib/presentation/actions/share_link.action.dart b/mobile/lib/presentation/actions/share_link.action.dart new file mode 100644 index 0000000000..1966dd8811 --- /dev/null +++ b/mobile/lib/presentation/actions/share_link.action.dart @@ -0,0 +1,33 @@ +import 'dart:async'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/routing/router.dart'; + +final _stateProvider = Provider.family.autoDispose?, ActionSource>((ref, source) { + final assets = ref.watch(assetsActionProvider(source)); + final remoteIds = assets.remote().map((asset) => asset.id).toList(growable: false); + return remoteIds.isEmpty ? null : remoteIds; +}); + +class ShareLinkAction extends AssetActionBuilder { + const ShareLinkAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + final remoteIds = ref.watch(_stateProvider(source)); + if (remoteIds == null) { + return null; + } + + return .new( + icon: Icons.link_rounded, + label: context.t.share_link, + onAction: () async => unawaited(context.pushRoute(SharedLinkEditRoute(assetsList: remoteIds))), + ); + } +} 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 deleted file mode 100644 index eadcf0a81e..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/share_action_button.widget.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -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/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'; -import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class _SharePreparingDialog extends StatelessWidget { - final ValueNotifier progress; - - const _SharePreparingDialog({required this.progress}); - - @override - Widget build(BuildContext context) { - return AlertDialog( - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container(margin: const EdgeInsets.only(bottom: 12), child: const Text('share_dialog_preparing').tr()), - SizedBox( - width: 240, - child: ValueListenableBuilder( - valueListenable: progress, - builder: (context, value, _) { - final percent = value == null ? null : (value * 100).clamp(0, 100); - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - LinearProgressIndicator(value: value, minHeight: 8.0), - if (percent != null) - Container(margin: const EdgeInsets.only(top: 8), child: Text('${percent.toStringAsFixed(0)}%')), - ], - ); - }, - ), - ), - ], - ), - ); - } -} - -class _ShareFileTypeDialog extends StatelessWidget { - final bool showPreview; - - const _ShareFileTypeDialog({this.showPreview = true}); - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: Text(context.t.select_quality), - contentPadding: const EdgeInsets.symmetric(vertical: 8), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: const Icon(Icons.high_quality_rounded), - title: Text(context.t.share_original), - onTap: () => context.pop(ShareAssetType.original), - ), - if (showPreview) - ListTile( - leading: const Icon(Icons.photo_size_select_large_rounded), - title: Text(context.t.share_preview), - onTap: () => context.pop(ShareAssetType.preview), - ), - ], - ), - actions: [TextButton(onPressed: () => context.pop(), child: Text(context.t.cancel))], - ); - } -} - -class ShareActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const ShareActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - Set _getSelectedAssets(WidgetRef ref) { - return switch (source) { - ActionSource.timeline => ref.read(multiSelectProvider).selectedAssets, - ActionSource.viewer => switch (ref.read(assetViewerProvider).currentAsset) { - final BaseAsset asset => {asset}, - null => const {}, - }, - }; - } - - Future _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final fileType = ref.read(appConfigProvider).share.fileType; - await _share(context, ref, fileType); - } - - Future _onLongPress(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - // only show preview option when at least one of the assets is not a video - // we cant share previews of videos - final assets = _getSelectedAssets(ref); - final showPreview = assets.isEmpty || assets.any((asset) => !asset.isVideo); - - final fileType = await showDialog( - context: context, - builder: (_) => _ShareFileTypeDialog(showPreview: showPreview), - useRootNavigator: false, - ); - - if (fileType == null || !context.mounted) { - return; - } - - await _share(context, ref, fileType); - } - - Future _share(BuildContext context, WidgetRef ref, ShareAssetType fileType) async { - final cancelCompleter = Completer(); - final progress = ValueNotifier(null); - final preparingDialog = _SharePreparingDialog(progress: progress); - await showDialog( - context: context, - builder: (BuildContext buildContext) { - unawaited( - ref - .read(actionProvider.notifier) - .shareAssets( - source, - context, - fileType: fileType, - cancelCompleter: cancelCompleter, - onAssetDownloadProgress: (value) => progress.value = value, - ) - .then((ActionResult result) { - if (cancelCompleter.isCompleted || !context.mounted) { - return; - } - - if (!result.success) { - ImmichToast.show( - context: context, - msg: context.t.scaffold_body_error_occurred, - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - } - - buildContext.pop(); - }), - ); - - return preparingDialog; - }, - barrierDismissible: false, - useRootNavigator: false, - ).then((_) { - if (!cancelCompleter.isCompleted) { - cancelCompleter.complete(); - } - progress.dispose(); - }); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded, - label: context.t.share, - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - onLongPressed: () => _onLongPress(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/share_link_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/share_link_action_button.widget.dart deleted file mode 100644 index dfe8fad025..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/share_link_action_button.widget.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; - -class ShareLinkActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const ShareLinkActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - Future _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - await ref.read(actionProvider.notifier).shareLink(source, context); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.link_rounded, - label: "share_link".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart index e86088cb2d..c1f6ec8b47 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -7,8 +7,8 @@ import 'package:immich_mobile/presentation/actions/action.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; import 'package:immich_mobile/presentation/actions/edit_asset.action.dart'; import 'package:immich_mobile/presentation/actions/restore.action.dart'; +import 'package:immich_mobile/presentation/actions/share.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/ocr_toggle_button.widget.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; @@ -45,7 +45,7 @@ class ViewerBottomBar extends ConsumerWidget { final actions = [ ..._actionColumnButtons(context, ref, const [RestoreAction(source: .viewer)]), - const ShareActionButton(source: .viewer), + ..._actionColumnButtons(context, ref, const [ShareAction(source: .viewer)]), if (!isInLockedView) ...[ if (!isInTrash) ...[ diff --git a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart index 9290e157b3..e8499ad967 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart @@ -10,10 +10,10 @@ import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart'; import 'package:immich_mobile/presentation/actions/edit_location.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; +import 'package:immich_mobile/presentation/actions/share.action.dart'; +import 'package:immich_mobile/presentation/actions/share_link.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; @@ -76,9 +76,9 @@ class _ArchiveBottomSheetState extends ConsumerState { maxChildSize: 0.85, shouldCloseOnMinExtent: false, actions: [ - const ShareActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: ShareAction(source: .timeline)), if (multiselect.hasRemote) ...[ - const ShareLinkActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: ShareLinkAction(source: .timeline)), const ActionColumnButton(action: ArchiveAction(source: .timeline)), const ActionColumnButton(action: FavoriteAction(source: .timeline)), if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart index ac0dd14922..154ba29fa1 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart @@ -11,10 +11,10 @@ import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart'; import 'package:immich_mobile/presentation/actions/edit_location.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; +import 'package:immich_mobile/presentation/actions/share.action.dart'; +import 'package:immich_mobile/presentation/actions/share_link.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; @@ -76,9 +76,9 @@ class FavoriteBottomSheet extends ConsumerWidget { maxChildSize: 0.7, shouldCloseOnMinExtent: false, actions: [ - const ShareActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: ShareAction(source: .timeline)), if (multiselect.hasRemote) ...[ - const ShareLinkActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: ShareLinkAction(source: .timeline)), const ActionColumnButton(action: FavoriteAction(source: .timeline)), const ActionColumnButton(action: ArchiveAction(source: .timeline)), if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart index 0ca4af56ce..e21dedc1e3 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart @@ -10,11 +10,11 @@ import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart'; import 'package:immich_mobile/presentation/actions/edit_location.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; +import 'package:immich_mobile/presentation/actions/share.action.dart'; +import 'package:immich_mobile/presentation/actions/share_link.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; @@ -83,9 +83,9 @@ class _GeneralBottomSheetState extends ConsumerState { shouldCloseOnMinExtent: false, actions: [ const ActionColumnButton(action: FavoriteAction(source: .timeline)), - const ShareActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: ShareAction(source: .timeline)), if (multiselect.hasRemote) ...[ - const ShareLinkActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: ShareLinkAction(source: .timeline)), if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline), const ActionColumnButton(action: FavoriteAction(source: .timeline)), const ActionColumnButton(action: ArchiveAction(source: .timeline)), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart index dc49f22cfc..3e8d982a21 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart @@ -5,7 +5,7 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; +import 'package:immich_mobile/presentation/actions/share.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; @@ -66,7 +66,7 @@ class _LocalAlbumBottomSheetState extends ConsumerState { maxChildSize: 0.85, shouldCloseOnMinExtent: false, actions: const [ - ShareActionButton(source: ActionSource.timeline), + ActionColumnButton(action: ShareAction(source: .timeline)), ActionColumnButton(action: DeleteAction(source: .timeline)), ActionColumnButton(action: CleanupLocalAction(source: .timeline)), UploadActionButton(source: ActionSource.timeline), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart index e0074c7866..3a9d35b46d 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart @@ -4,8 +4,8 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; +import 'package:immich_mobile/presentation/actions/share.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; class LockedFolderBottomSheet extends ConsumerWidget { @@ -18,7 +18,7 @@ class LockedFolderBottomSheet extends ConsumerWidget { maxChildSize: 0.4, shouldCloseOnMinExtent: false, actions: [ - ShareActionButton(source: ActionSource.timeline), + ActionColumnButton(action: ShareAction(source: .timeline)), DownloadActionButton(source: ActionSource.timeline), ActionColumnButton(action: DeleteAction(source: .timeline)), ActionColumnButton(action: LockAction(source: .timeline)), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart index c49c805e3d..84d6b4478b 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart @@ -3,8 +3,9 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/share.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; @@ -64,7 +65,7 @@ class _PartnerDetailBottomSheetState extends ConsumerState maxChildSize: 0.85, shouldCloseOnMinExtent: false, actions: [ - const ShareActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: ShareAction(source: .timeline)), if (multiselect.hasRemote) ...[ - const ShareLinkActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: ShareLinkAction(source: .timeline)), if (ownsAlbum) ...[ const ActionColumnButton(action: ArchiveAction(source: .timeline)), diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index d9ca0df303..e5eb10f550 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -82,17 +82,6 @@ class ActionNotifier extends Notifier { return ActionResult(count: assets.length, success: true); } - Future shareLink(ActionSource source, BuildContext context) async { - final ids = _getRemoteIdsForSource(source); - try { - await _service.shareLink(ids, context); - return ActionResult(count: ids.length, success: true); - } catch (error, stack) { - _logger.severe('Failed to create shared link for assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - Future emptyTrash(String userId) async { try { final count = await _service.emptyTrash(userId); @@ -239,30 +228,6 @@ class ActionNotifier extends Notifier { } } - Future shareAssets( - ActionSource source, - BuildContext context, { - ShareAssetType fileType = ShareAssetType.original, - Completer? cancelCompleter, - void Function(double progress)? onAssetDownloadProgress, - }) async { - final ids = _getAssets(source).toList(growable: false); - - try { - final count = await _service.shareAssets( - ids, - context, - fileType: fileType, - cancelCompleter: cancelCompleter, - onAssetDownloadProgress: onAssetDownloadProgress, - ); - return ActionResult(count: count, success: count > 0 || ids.isEmpty); - } catch (error, stack) { - _logger.severe('Failed to share assets', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - Future downloadAll(ActionSource source) async { final assets = _getAssets(source).whereType().toList(growable: false); try { diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index 415a9c578b..7126953ad9 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -1,9 +1,7 @@ import 'dart:async'; -import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.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/services/tag.service.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; @@ -11,10 +9,8 @@ import 'package:immich_mobile/infrastructure/repositories/remote_asset.repositor import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; -import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/repositories/download.repository.dart'; import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; -import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/common/tag_picker.dart'; final actionServiceProvider = Provider( @@ -23,7 +19,6 @@ final actionServiceProvider = Provider( ref.watch(remoteAssetRepositoryProvider), ref.watch(driftAlbumApiRepositoryProvider), ref.watch(remoteAlbumRepository), - ref.watch(assetMediaRepositoryProvider), ref.watch(downloadRepositoryProvider), ref.watch(tagServiceProvider), ), @@ -34,7 +29,6 @@ class ActionService { final RemoteAssetRepository _remoteAssetRepository; final DriftAlbumApiRepository _albumApiRepository; final DriftRemoteAlbumRepository _remoteAlbumRepository; - final AssetMediaRepository _assetMediaRepository; final DownloadRepository _downloadRepository; final TagService _tagService; @@ -43,15 +37,10 @@ class ActionService { this._remoteAssetRepository, this._albumApiRepository, this._remoteAlbumRepository, - this._assetMediaRepository, this._downloadRepository, this._tagService, ); - Future shareLink(List remoteIds, BuildContext context) async { - unawaited(context.pushRoute(SharedLinkEditRoute(assetsList: remoteIds))); - } - Future emptyTrash(String userId) async { final count = await _assetApiRepository.emptyTrash(); await _remoteAssetRepository.emptyTrash(userId); @@ -108,22 +97,6 @@ class ActionService { return _tagService.bulkTagAssets(remoteIds, selectedTagIds.toList()); } - Future shareAssets( - List assets, - BuildContext context, { - ShareAssetType fileType = ShareAssetType.original, - Completer? cancelCompleter, - void Function(double progress)? onAssetDownloadProgress, - }) { - return _assetMediaRepository.shareAssets( - assets, - context, - fileType: fileType, - cancelCompleter: cancelCompleter, - onAssetDownloadProgress: onAssetDownloadProgress, - ); - } - Future> downloadAll(List assets) { return _downloadRepository.downloadAllAssets(assets); } diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 41015fb064..60ffa84066 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -16,6 +16,8 @@ import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/actions/open_in_browser.action.dart'; import 'package:immich_mobile/presentation/actions/restore.action.dart'; import 'package:immich_mobile/presentation/actions/set_profile_picture.action.dart'; +import 'package:immich_mobile/presentation/actions/share.action.dart'; +import 'package:immich_mobile/presentation/actions/share_link.action.dart'; import 'package:immich_mobile/presentation/actions/similar_photos.action.dart'; import 'package:immich_mobile/presentation/actions/slideshow.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; @@ -24,8 +26,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/download_actio import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -173,12 +173,8 @@ enum ActionButtonType { ]) { return switch (this) { ActionButtonType.advancedInfo => ActionMenuItem(action: AssetDebugAction(source: context.source)), - ActionButtonType.share => ShareActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), - ActionButtonType.shareLink => ShareLinkActionButton( - source: context.source, - iconOnly: iconOnly, - menuItem: menuItem, - ), + ActionButtonType.share => ActionMenuItem(action: ShareAction(source: context.source)), + ActionButtonType.shareLink => ActionMenuItem(action: ShareLinkAction(source: context.source)), ActionButtonType.slideshow => const ActionMenuItem(action: SlideshowAction()), ActionButtonType.archive || ActionButtonType.unarchive => ActionMenuItem(action: ArchiveAction(source: context.source)), diff --git a/mobile/test/services/action.service_test.dart b/mobile/test/services/action.service_test.dart index 0a1fbf08bc..2ab2389056 100644 --- a/mobile/test/services/action.service_test.dart +++ b/mobile/test/services/action.service_test.dart @@ -19,7 +19,6 @@ void main() { late MockRemoteAssetRepository remoteAssetRepository; late MockDriftAlbumApiRepository albumApiRepository; late MockRemoteAlbumRepository remoteAlbumRepository; - late MockAssetMediaRepository assetMediaRepository; late MockDownloadRepository downloadRepository; late MockTagService tagService; @@ -44,7 +43,6 @@ void main() { remoteAssetRepository = MockRemoteAssetRepository(); albumApiRepository = MockDriftAlbumApiRepository(); remoteAlbumRepository = MockRemoteAlbumRepository(); - assetMediaRepository = MockAssetMediaRepository(); downloadRepository = MockDownloadRepository(); tagService = MockTagService(); @@ -53,7 +51,6 @@ void main() { remoteAssetRepository, albumApiRepository, remoteAlbumRepository, - assetMediaRepository, downloadRepository, tagService, ); @@ -88,5 +85,4 @@ void main() { verify(() => remoteAssetRepository.updateRating(assetId, null)).called(1); }); }); - } 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 deleted file mode 100644 index 2f4aa3b8c9..0000000000 --- a/mobile/test/unit/presentation/action_buttons/share_action_button_test.dart +++ /dev/null @@ -1,117 +0,0 @@ -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); - }); - }); -} diff --git a/mobile/test/unit/presentation/actions/share_action_test.dart b/mobile/test/unit/presentation/actions/share_action_test.dart new file mode 100644 index 0000000000..9faa6e3a5f --- /dev/null +++ b/mobile/test/unit/presentation/actions/share_action_test.dart @@ -0,0 +1,159 @@ +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.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/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/share.action.dart'; +import 'package:immich_mobile/presentation/actions/share_link.action.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../factories/local_asset_factory.dart'; +import '../../factories/remote_asset_factory.dart'; +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + + setUpAll(() async { + final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + await SettingsRepository.ensureInitialized(db); + }); + + setUp(() async { + context = await PresentationContext.create(); + await SettingsRepository.instance.clear([SettingsKey.shareFileType]); + }); + + tearDown(() { + context.dispose(); + }); + + late BuildContext actionContext; + late WidgetRef actionRef; + + Future pumpShare(WidgetTester tester, {Set? selection}) => tester.pumpTestWidget( + context, + Consumer( + builder: (widgetContext, ref, _) { + actionContext = widgetContext; + actionRef = ref; + return const ActionIconButton(action: ShareAction(source: .timeline)); + }, + ), + overrides: [ + ...context.selected(selection ?? {RemoteAssetFactory.create(ownerId: context.currentUser.id)}), + ], + ); + + // TODO: Replace with button tap once long press support in ui is merged + Future invokeSecondaryAction() async { + final resolved = const ShareAction(source: .timeline).create(actionContext, actionRef); + await resolved!.onSecondaryAction!(); + } + + List sharedFileTypes() => verify( + () => context.repository.assetMedia.api.shareAssets( + any(), + any(), + fileType: captureAny(named: 'fileType'), + cancelCompleter: any(named: 'cancelCompleter'), + onAssetDownloadProgress: any(named: 'onAssetDownloadProgress'), + ), + ).captured.cast(); + + Future settle(WidgetTester tester) async { + await tester.pump(); + await tester.pump(const .new(milliseconds: 300)); + } + + Future pickPreviewQuality(WidgetTester tester) async { + final shared = invokeSecondaryAction(); + await settle(tester); + await tester.tap(find.byIcon(Icons.photo_size_select_large_rounded)); + await settle(tester); + await shared; + } + + group('ShareAction', () { + testWidgets('single press shares with the configured default quality', (tester) async { + await pumpShare(tester); + + await tester.tap(find.byType(ImmichIconButton)); + await settle(tester); + + expect(sharedFileTypes(), [ShareAssetType.original]); + }); + + testWidgets('the secondary action shares with the quality picked in the dialog', (tester) async { + await pumpShare(tester); + + await pickPreviewQuality(tester); + + expect(sharedFileTypes(), [ShareAssetType.preview]); + }); + + testWidgets('quality picked there is a one-time choice and does not change the default', (tester) async { + await pumpShare(tester); + + await pickPreviewQuality(tester); + await tester.tap(find.byType(ImmichIconButton)); + await settle(tester); + + expect(sharedFileTypes(), [ShareAssetType.preview, ShareAssetType.original]); + expect(SettingsRepository.instance.appConfig.share.fileType, ShareAssetType.original); + }); + + testWidgets('offers no preview option for a video, which has none to share', (tester) async { + await pumpShare( + tester, + selection: {RemoteAssetFactory.create(ownerId: context.currentUser.id, type: .video)}, + ); + + final shared = invokeSecondaryAction(); + await settle(tester); + + expect(find.byIcon(Icons.high_quality_rounded), findsOneWidget); + expect(find.byIcon(Icons.photo_size_select_large_rounded), findsNothing); + + await tester.tap(find.byIcon(Icons.high_quality_rounded)); + await settle(tester); + await shared; + expect(find.byIcon(Icons.photo_size_select_large_rounded), findsNothing); + }); + + testWidgets('is hidden when nothing is selected', (tester) async { + await pumpShare(tester, selection: const {}); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); + + group('ShareLinkAction', () { + testWidgets('offers a link for a remote asset', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: ShareLinkAction(source: .timeline)), + overrides: context.selected({RemoteAssetFactory.create()}), + ); + + expect(find.byType(ImmichIconButton), findsOneWidget); + }); + + testWidgets('is hidden for a local-only asset', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: ShareLinkAction(source: .timeline)), + overrides: context.selected({LocalAssetFactory.create()}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); +} diff --git a/mobile/test/unit/presentation/presentation_context.dart b/mobile/test/unit/presentation/presentation_context.dart index ba12183652..bfd8e877d8 100644 --- a/mobile/test/unit/presentation/presentation_context.dart +++ b/mobile/test/unit/presentation/presentation_context.dart @@ -19,6 +19,7 @@ import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/services/cleanup.service.dart'; import 'package:immich_mobile/services/gcast.service.dart'; import 'package:immich_mobile/services/server_info.service.dart'; @@ -55,6 +56,7 @@ class PresentationContext { serverInfoServiceProvider.overrideWithValue(service.serverInfo), inLockedViewProvider.overrideWithValue(false), remoteAssetRepositoryProvider.overrideWithValue(repository.remoteAsset.repo), + assetMediaRepositoryProvider.overrideWithValue(repository.assetMedia.api), ]; List selected(Set assets) => [ From 1fa985babb97bb628a8fac16a4c4c6a455d27b2a Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Sat, 1 Aug 2026 08:06:43 +0530 Subject: [PATCH 40/81] feat: album action (#29770) refactor: mobile album actions Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../actions/remove_from_album.action.dart | 52 +++++++ .../actions/set_album_cover.action.dart | 49 +++++++ ...emove_from_album_action_button.widget.dart | 65 --------- .../set_album_cover.widget.dart | 56 -------- .../remote_album_bottom_sheet.widget.dart | 15 ++- .../infrastructure/action.provider.dart | 27 ---- mobile/lib/services/action.service.dart | 24 ---- mobile/lib/utils/action_button.utils.dart | 18 +-- mobile/test/services/action.service_test.dart | 13 +- .../actions/album_action_test.dart | 127 ++++++++++++++++++ .../presentation/presentation_context.dart | 2 + 11 files changed, 247 insertions(+), 201 deletions(-) create mode 100644 mobile/lib/presentation/actions/remove_from_album.action.dart create mode 100644 mobile/lib/presentation/actions/set_album_cover.action.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/set_album_cover.widget.dart create mode 100644 mobile/test/unit/presentation/actions/album_action_test.dart diff --git a/mobile/lib/presentation/actions/remove_from_album.action.dart b/mobile/lib/presentation/actions/remove_from_album.action.dart new file mode 100644 index 0000000000..3d648a9cfd --- /dev/null +++ b/mobile/lib/presentation/actions/remove_from_album.action.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; + +final _stateProvider = Provider.family.autoDispose?, ActionSource>((ref, source) { + final assets = ref.watch(assetsActionProvider(source)); + final assetIds = assets.remote().map((asset) => asset.id).toList(growable: false); + return assetIds.isEmpty ? null : assetIds; +}); + +class RemoveFromAlbumAction extends AssetActionBuilder { + final String albumId; + + const RemoveFromAlbumAction({required super.source, required this.albumId}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + final assetIds = ref.watch(_stateProvider(source)); + if (assetIds == null) { + return null; + } + + return .new( + icon: Icons.remove_circle_outline, + label: context.t.remove_from_album, + onAction: () => _remove(context, ref, assetIds), + ); + } + + Future _remove(BuildContext context, WidgetRef ref, List assetIds) async { + final albumService = ref.read(remoteAlbumServiceProvider); + final toastService = ref.read(toastServiceProvider); + final clearSelection = ref.read(clearSelectionProvider(source)); + + try { + final count = await albumService.removeAssets(albumId: albumId, assetIds: assetIds); + if (!context.mounted) { + return; + } + + toastService.success(context.t.remove_from_album_action_prompt(count: count)); + clearSelection(); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to remove the assets from the album"); + } + } +} diff --git a/mobile/lib/presentation/actions/set_album_cover.action.dart b/mobile/lib/presentation/actions/set_album_cover.action.dart new file mode 100644 index 0000000000..0c16c9a9db --- /dev/null +++ b/mobile/lib/presentation/actions/set_album_cover.action.dart @@ -0,0 +1,49 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +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; +}); + +class SetAlbumCoverAction extends AssetActionBuilder { + final String albumId; + + const SetAlbumCoverAction({required super.source, required this.albumId}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + final assetId = ref.watch(_stateProvider(source)); + if (assetId == null) { + return null; + } + + return .new( + icon: Icons.image_outlined, + label: context.t.set_as_album_cover, + onAction: () => _setCover(context, ref, assetId), + ); + } + + Future _setCover(BuildContext context, WidgetRef ref, String assetId) async { + final message = context.t.album_cover_updated; + final albumService = ref.read(remoteAlbumServiceProvider); + final toastService = ref.read(toastServiceProvider); + final clearSelection = ref.read(clearSelectionProvider(source)); + + try { + await albumService.updateAlbum(albumId, thumbnailAssetId: assetId); + toastService.success(message); + clearSelection(); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to update the album cover"); + } + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart deleted file mode 100644 index 7049da13f8..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/material.dart'; -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/events.model.dart'; -import 'package:immich_mobile/domain/utils/event_stream.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class RemoveFromAlbumActionButton extends ConsumerWidget { - final String albumId; - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const RemoveFromAlbumActionButton({ - super.key, - required this.albumId, - required this.source, - this.iconOnly = false, - this.menuItem = false, - }); - - Future _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - if (source == ActionSource.viewer) { - EventStream.shared.emit(const ViewerReloadAssetEvent()); - } - - final result = await ref.read(actionProvider.notifier).removeFromAlbum(source, albumId); - ref.read(multiSelectProvider.notifier).reset(); - if (!context.mounted) { - return; - } - - final successMessage = 'remove_from_album_action_prompt'.t( - context: context, - args: {'count': result.count.toString()}, - ); - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.remove_circle_outline, - label: "remove_from_album".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - maxWidth: 100, - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/set_album_cover.widget.dart b/mobile/lib/presentation/widgets/action_buttons/set_album_cover.widget.dart deleted file mode 100644 index e6e572110e..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/set_album_cover.widget.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class SetAlbumCoverActionButton extends ConsumerWidget { - final String albumId; - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const SetAlbumCoverActionButton({ - super.key, - required this.albumId, - required this.source, - this.iconOnly = false, - this.menuItem = false, - }); - - Future _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final result = await ref.read(actionProvider.notifier).setAlbumCover(source, albumId); - ref.read(multiSelectProvider.notifier).reset(); - if (!context.mounted) { - return; - } - - final successMessage = 'album_cover_updated'.t(context: context); - ImmichToast.show( - context: context, - msg: result.success ? successMessage : 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.image_outlined, - label: 'set_as_album_cover'.t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - maxWidth: 100, - ); - } -} diff --git a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart index 21537a664d..a070b1c2ab 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart @@ -10,12 +10,12 @@ import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart'; import 'package:immich_mobile/presentation/actions/edit_location.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; +import 'package:immich_mobile/presentation/actions/remove_from_album.action.dart'; +import 'package:immich_mobile/presentation/actions/set_album_cover.action.dart'; import 'package:immich_mobile/presentation/actions/share.action.dart'; import 'package:immich_mobile/presentation/actions/share_link.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; @@ -104,9 +104,14 @@ class _RemoteAlbumBottomSheetState extends ConsumerState ], ], const ActionColumnButton(action: CleanupLocalAction(source: .timeline)), - if (ownsAlbum) RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id), - if (ownsAlbum && multiselect.selectedAssets.length == 1) - SetAlbumCoverActionButton(source: ActionSource.timeline, albumId: widget.album.id), + if (ownsAlbum) ...[ + ActionColumnButton( + action: RemoveFromAlbumAction(source: .timeline, albumId: widget.album.id), + ), + ActionColumnButton( + action: SetAlbumCoverAction(source: .timeline, albumId: widget.album.id), + ), + ], ], slivers: ownsAlbum ? [const AddToAlbumHeader(), AlbumSelector(onAlbumSelected: addToAlbum, onKeyboardExpanded: onKeyboardExpand)] diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index e5eb10f550..c10ac37b22 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -169,33 +169,6 @@ class ActionNotifier extends Notifier { ); } - Future removeFromAlbum(ActionSource source, String albumId) async { - final ids = _getRemoteIdsForSource(source); - try { - final removedCount = await _service.removeFromAlbum(ids, albumId); - return ActionResult(count: removedCount, success: true); - } catch (error, stack) { - _logger.severe('Failed to remove assets from album', error, stack); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - - Future setAlbumCover(ActionSource source, String albumId) async { - final assets = _getAssets(source); - final asset = assets.first; - if (asset is! RemoteAsset) { - return const ActionResult(count: 1, success: false, error: 'Asset must be remote'); - } - - try { - await _service.setAlbumCover(albumId, asset.id); - return const ActionResult(count: 1, success: true); - } catch (error, stack) { - _logger.severe('Failed to set album cover', error, stack); - return ActionResult(count: 1, success: false, error: error.toString()); - } - } - Future updateDescription(ActionSource source, String description) async { final ids = _getRemoteIdsForSource(source); if (ids.length != 1) { diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index 7126953ad9..90e7978c97 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -4,21 +4,16 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/services/tag.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; import 'package:immich_mobile/repositories/download.repository.dart'; -import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/widgets/common/tag_picker.dart'; final actionServiceProvider = Provider( (ref) => ActionService( ref.watch(assetApiRepositoryProvider), ref.watch(remoteAssetRepositoryProvider), - ref.watch(driftAlbumApiRepositoryProvider), - ref.watch(remoteAlbumRepository), ref.watch(downloadRepositoryProvider), ref.watch(tagServiceProvider), ), @@ -27,16 +22,12 @@ final actionServiceProvider = Provider( class ActionService { final AssetApiRepository _assetApiRepository; final RemoteAssetRepository _remoteAssetRepository; - final DriftAlbumApiRepository _albumApiRepository; - final DriftRemoteAlbumRepository _remoteAlbumRepository; final DownloadRepository _downloadRepository; final TagService _tagService; const ActionService( this._assetApiRepository, this._remoteAssetRepository, - this._albumApiRepository, - this._remoteAlbumRepository, this._downloadRepository, this._tagService, ); @@ -53,14 +44,6 @@ class ActionService { return count; } - Future removeFromAlbum(List remoteIds, String albumId) async { - final result = await _albumApiRepository.removeAssets(albumId, remoteIds); - if (result.removed.isNotEmpty) { - await _remoteAlbumRepository.removeAssets(albumId, result.removed); - } - return result.removed.length; - } - Future updateDescription(String assetId, String description) async { // update remote first, then local to ensure consistency await _assetApiRepository.updateDescription(assetId, description); @@ -100,11 +83,4 @@ class ActionService { Future> downloadAll(List assets) { return _downloadRepository.downloadAllAssets(assets); } - - Future setAlbumCover(String albumId, String assetId) async { - final owner = await _remoteAlbumRepository.getOwner(albumId); - final updatedAlbum = await _albumApiRepository.updateAlbum(albumId, owner, thumbnailAssetId: assetId); - await _remoteAlbumRepository.update(updatedAlbum); - return true; - } } diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 60ffa84066..7a416a4441 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -14,7 +14,9 @@ import 'package:immich_mobile/presentation/actions/cast.action.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/actions/open_in_browser.action.dart'; +import 'package:immich_mobile/presentation/actions/remove_from_album.action.dart'; import 'package:immich_mobile/presentation/actions/restore.action.dart'; +import 'package:immich_mobile/presentation/actions/set_album_cover.action.dart'; import 'package:immich_mobile/presentation/actions/set_profile_picture.action.dart'; import 'package:immich_mobile/presentation/actions/share.action.dart'; import 'package:immich_mobile/presentation/actions/share_link.action.dart'; @@ -24,8 +26,6 @@ import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -185,17 +185,11 @@ enum ActionButtonType { ActionButtonType.removeFromLockFolder => ActionMenuItem(action: LockAction(source: context.source)), ActionButtonType.deleteLocal => ActionMenuItem(action: CleanupLocalAction(source: context.source)), ActionButtonType.upload => UploadActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), - ActionButtonType.removeFromAlbum => RemoveFromAlbumActionButton( - albumId: context.currentAlbum!.id, - source: context.source, - iconOnly: iconOnly, - menuItem: menuItem, + ActionButtonType.removeFromAlbum => ActionMenuItem( + action: RemoveFromAlbumAction(source: context.source, albumId: context.currentAlbum!.id), ), - ActionButtonType.setAlbumCover => SetAlbumCoverActionButton( - albumId: context.currentAlbum!.id, - source: context.source, - iconOnly: iconOnly, - menuItem: menuItem, + ActionButtonType.setAlbumCover => ActionMenuItem( + action: SetAlbumCoverAction(source: context.source, albumId: context.currentAlbum!.id), ), ActionButtonType.likeActivity => LikeActivityActionButton(iconOnly: iconOnly, menuItem: menuItem), ActionButtonType.unstack => ActionMenuItem(action: StackAction(source: context.source)), diff --git a/mobile/test/services/action.service_test.dart b/mobile/test/services/action.service_test.dart index 2ab2389056..389d4200da 100644 --- a/mobile/test/services/action.service_test.dart +++ b/mobile/test/services/action.service_test.dart @@ -17,8 +17,6 @@ void main() { late MockAssetApiRepository assetApiRepository; late MockRemoteAssetRepository remoteAssetRepository; - late MockDriftAlbumApiRepository albumApiRepository; - late MockRemoteAlbumRepository remoteAlbumRepository; late MockDownloadRepository downloadRepository; late MockTagService tagService; @@ -41,19 +39,10 @@ void main() { setUp(() { assetApiRepository = MockAssetApiRepository(); remoteAssetRepository = MockRemoteAssetRepository(); - albumApiRepository = MockDriftAlbumApiRepository(); - remoteAlbumRepository = MockRemoteAlbumRepository(); downloadRepository = MockDownloadRepository(); tagService = MockTagService(); - sut = ActionService( - assetApiRepository, - remoteAssetRepository, - albumApiRepository, - remoteAlbumRepository, - downloadRepository, - tagService, - ); + sut = ActionService(assetApiRepository, remoteAssetRepository, downloadRepository, tagService); }); tearDown(() async { diff --git a/mobile/test/unit/presentation/actions/album_action_test.dart b/mobile/test/unit/presentation/actions/album_action_test.dart new file mode 100644 index 0000000000..1cf413cbdc --- /dev/null +++ b/mobile/test/unit/presentation/actions/album_action_test.dart @@ -0,0 +1,127 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/remove_from_album.action.dart'; +import 'package:immich_mobile/presentation/actions/set_album_cover.action.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../service.mocks.dart'; +import '../../factories/local_asset_factory.dart'; +import '../../factories/remote_album_factory.dart'; +import '../../factories/remote_asset_factory.dart'; +import '../presentation_context.dart'; + +void main() { + const albumId = 'album-1'; + + late PresentationContext context; + late MockRemoteAlbumService albumService; + + setUp(() async { + context = await PresentationContext.create(); + albumService = context.service.album.service; + }); + + tearDown(() { + context.dispose(); + }); + + List withMockToast() => [toastServiceProvider.overrideWithValue(context.service.toast)]; + + group('RemoveFromAlbumAction', () { + Future pumpRemove(WidgetTester tester, Set selection) => tester.pumpTestAction( + context, + const RemoveFromAlbumAction(source: .timeline, albumId: albumId), + overrides: [...context.selected(selection), ...withMockToast()], + ); + + testWidgets('removes every selected remote asset from the album', (tester) async { + final first = RemoteAssetFactory.create(); + final second = RemoteAssetFactory.create(); + + await pumpRemove(tester, {first, second}); + await tester.pumpAndSettle(); + + verify(() => albumService.removeAssets(albumId: albumId, assetIds: [first.id, second.id])).called(1); + }); + + testWidgets('reports the count the server actually removed', (tester) async { + when( + () => albumService.removeAssets( + albumId: any(named: 'albumId'), + assetIds: any(named: 'assetIds'), + ), + ).thenAnswer((_) async => 1); + + await pumpRemove(tester, {RemoteAssetFactory.create(), RemoteAssetFactory.create()}); + await tester.pumpAndSettle(); + + final message = verify(() => context.service.toast.success(captureAny())).captured.single as String; + expect(message, StaticTranslations.instance.remove_from_album_action_prompt(count: 1)); + }); + + testWidgets('is hidden for a local-only asset', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton( + action: RemoveFromAlbumAction(source: .timeline, albumId: albumId), + ), + overrides: context.selected({LocalAssetFactory.create()}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); + + group('SetAlbumCoverAction', () { + testWidgets('sets the single selected asset as the cover', (tester) async { + final asset = RemoteAssetFactory.create(); + when( + () => albumService.updateAlbum(any(), thumbnailAssetId: any(named: 'thumbnailAssetId')), + ).thenAnswer((_) async => RemoteAlbumFactory.create(id: albumId)); + + await tester.pumpTestAction( + context, + const SetAlbumCoverAction(source: .timeline, albumId: albumId), + overrides: [ + ...context.selected({asset}), + ...withMockToast(), + ], + ); + await tester.pumpAndSettle(); + + verify(() => albumService.updateAlbum(albumId, thumbnailAssetId: asset.id)).called(1); + + final message = verify(() => context.service.toast.success(captureAny())).captured.single as String; + expect(message, StaticTranslations.instance.album_cover_updated); + }); + + testWidgets('is hidden for more than one asset', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton( + action: SetAlbumCoverAction(source: .timeline, albumId: albumId), + ), + overrides: context.selected({RemoteAssetFactory.create(), RemoteAssetFactory.create()}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('is hidden for a local-only asset', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton( + action: SetAlbumCoverAction(source: .timeline, albumId: albumId), + ), + overrides: context.selected({LocalAssetFactory.create()}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); +} diff --git a/mobile/test/unit/presentation/presentation_context.dart b/mobile/test/unit/presentation/presentation_context.dart index bfd8e877d8..43c76c8bf8 100644 --- a/mobile/test/unit/presentation/presentation_context.dart +++ b/mobile/test/unit/presentation/presentation_context.dart @@ -14,6 +14,7 @@ import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/presentation/actions/action.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/user.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; @@ -51,6 +52,7 @@ class PresentationContext { currentUserProvider.overrideWith((ref) => CurrentUserProvider(service.user.service)), assetServiceProvider.overrideWithValue(service.asset.service), cleanupServiceProvider.overrideWithValue(service.cleanup.service), + remoteAlbumServiceProvider.overrideWithValue(service.album.service), partnerServiceProvider.overrideWithValue(service.partner.service), gCastServiceProvider.overrideWithValue(service.cast), serverInfoServiceProvider.overrideWithValue(service.serverInfo), From bc84054cccb4be37c447d1b9277ea440e108b454 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Sat, 1 Aug 2026 08:06:43 +0530 Subject: [PATCH 41/81] refactor: mobile tag and download actions (#29948) refactor: mobile tag and download action Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../presentation/actions/download.action.dart | 49 ++++++ .../lib/presentation/actions/tag.action.dart | 86 ++++++++++ .../bulk_tag_assets_action_button.widget.dart | 46 ----- .../download_action_button.widget.dart | 47 ------ .../archive_bottom_sheet.widget.dart | 4 +- .../favorite_bottom_sheet.widget.dart | 5 +- .../general_bottom_sheet.widget.dart | 12 +- .../locked_folder_bottom_sheet.widget.dart | 5 +- .../partner_detail_bottom_sheet.widget.dart | 4 +- .../remote_album_bottom_sheet.widget.dart | 4 +- .../infrastructure/action.provider.dart | 43 ----- mobile/lib/services/action.service.dart | 45 +---- mobile/lib/utils/action_button.utils.dart | 4 +- mobile/test/services/action.service_test.dart | 6 +- .../actions/download_tag_action_test.dart | 159 ++++++++++++++++++ 15 files changed, 313 insertions(+), 206 deletions(-) create mode 100644 mobile/lib/presentation/actions/download.action.dart create mode 100644 mobile/lib/presentation/actions/tag.action.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart create mode 100644 mobile/test/unit/presentation/actions/download_tag_action_test.dart diff --git a/mobile/lib/presentation/actions/download.action.dart b/mobile/lib/presentation/actions/download.action.dart new file mode 100644 index 0000000000..1303fa6b03 --- /dev/null +++ b/mobile/lib/presentation/actions/download.action.dart @@ -0,0 +1,49 @@ +import 'dart:async'; + +import 'package:flutter/material.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/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; +import 'package:immich_mobile/repositories/download.repository.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; + +final _stateProvider = Provider.family.autoDispose?, ActionSource>((ref, source) { + final assets = ref.watch(assetsActionProvider(source)); + final remote = assets.remote().toList(growable: false); + return remote.isEmpty ? null : remote; +}); + +class DownloadAction extends AssetActionBuilder { + const DownloadAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + final assets = ref.watch(_stateProvider(source)); + if (assets == null) { + return null; + } + + return .new(icon: Icons.download, label: context.t.download, onAction: () => _download(ref, assets)); + } + + Future _download(WidgetRef ref, List assets) async { + final backgroundSync = ref.read(backgroundSyncProvider); + final downloads = ref.read(downloadRepositoryProvider); + + try { + await downloads.downloadAllAssets(assets); + + unawaited( + Future.delayed(const .new(seconds: 1), () async { + await backgroundSync.syncLocal(); + await backgroundSync.hashAssets(); + }), + ); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to download the assets"); + } + } +} diff --git a/mobile/lib/presentation/actions/tag.action.dart b/mobile/lib/presentation/actions/tag.action.dart new file mode 100644 index 0000000000..749b23d150 --- /dev/null +++ b/mobile/lib/presentation/actions/tag.action.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/services/tag.service.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/infrastructure/tag.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; +import 'package:immich_mobile/widgets/common/tag_picker.dart'; + +final _stateProvider = Provider.family.autoDispose?, ActionSource>((ref, source) { + final tagsEnabled = ref.watch( + userMetadataPreferencesProvider.select((value) => value.valueOrNull?.tagsEnabled ?? false), + ); + if (!tagsEnabled) { + return null; + } + + final assets = ref.watch(ownedAssetsActionProvider(source)); + final assetIds = assets.map((asset) => asset.id).toList(growable: false); + return assetIds.isEmpty ? null : assetIds; +}); + +class TagAction extends AssetActionBuilder { + const TagAction({required super.source}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + final assetIds = ref.watch(_stateProvider(source)); + if (assetIds == null) { + return null; + } + + return .new( + icon: Icons.sell_outlined, + label: context.t.control_bottom_app_bar_add_tags, + onAction: () => _tag(context, ref, assetIds), + ); + } + + Future _tag(BuildContext context, WidgetRef ref, List assetIds) async { + final clearSelection = ref.read(clearSelectionProvider(source)); + + try { + final results = await showTagPickerModal(context: context); + if (results == null || !context.mounted) { + return; + } + + final (selected, created) = results; + await tagAssets(context, ref, assetIds, selected: selected, created: created); + clearSelection(); + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to tag the assets"); + } + } +} + +@visibleForTesting +Future tagAssets( + BuildContext context, + WidgetRef ref, + List assetIds, { + required Set selected, + required Set created, +}) async { + final tagService = ref.read(tagServiceProvider); + final toastService = ref.read(toastServiceProvider); + final tagIds = {...selected}; + + if (created.isNotEmpty) { + final tags = await tagService.upsertTags(created.toList()); + tagIds.addAll(tags.map((tag) => tag.id)); + } + if (tagIds.isEmpty) { + return; + } + + final count = await tagService.bulkTagAssets(assetIds, tagIds.toList()); + ref.invalidate(tagProvider); + if (context.mounted) { + toastService.success(context.t.tagged_assets(count: count)); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart deleted file mode 100644 index b9ac47cd57..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; - -class BulkTagAssetsActionButton extends ConsumerWidget { - final ActionSource source; - - const BulkTagAssetsActionButton({super.key, required this.source}); - - Future _onTap(BuildContext context, WidgetRef ref) async { - final result = await ref.read(actionProvider.notifier).tagAssets(source, context); - if (result == null) { - return; - } - - ref.read(multiSelectProvider.notifier).reset(); - - if (!context.mounted) { - return; - } - - ImmichToast.show( - context: context, - msg: result.success - ? 'tagged_assets'.t(context: context, args: {'count': result.count.toString()}) - : 'errors.failed_to_tag_assets'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: result.success ? ToastType.success : ToastType.error, - ); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.sell_outlined, - label: "control_bottom_app_bar_add_tags".t(context: context), - onPressed: () => _onTap(context, ref), - ); - } -} diff --git a/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart deleted file mode 100644 index b6f8cc614e..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/download_action_button.widget.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; -import 'package:immich_mobile/domain/utils/background_sync.dart'; -import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; - -class DownloadActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - const DownloadActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - Future _onTap(BuildContext context, WidgetRef ref, BackgroundSyncManager backgroundSyncManager) async { - if (!context.mounted) { - return; - } - - try { - await ref.read(actionProvider.notifier).downloadAll(source); - - Future.delayed(const Duration(seconds: 1), () async { - await backgroundSyncManager.syncLocal(); - await backgroundSyncManager.hashAssets(); - }); - } finally { - ref.read(multiSelectProvider.notifier).reset(); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - final backgroundManager = ref.watch(backgroundSyncProvider); - - return BaseActionButton( - iconData: Icons.download, - maxWidth: 95, - label: "download".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref, backgroundManager), - ); - } -} diff --git a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart index e8499ad967..032ccda917 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart @@ -6,6 +6,7 @@ import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; +import 'package:immich_mobile/presentation/actions/download.action.dart'; import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart'; import 'package:immich_mobile/presentation/actions/edit_location.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; @@ -13,7 +14,6 @@ import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/actions/share.action.dart'; import 'package:immich_mobile/presentation/actions/share_link.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; @@ -81,7 +81,7 @@ class _ArchiveBottomSheetState extends ConsumerState { const ActionColumnButton(action: ShareLinkAction(source: .timeline)), const ActionColumnButton(action: ArchiveAction(source: .timeline)), const ActionColumnButton(action: FavoriteAction(source: .timeline)), - if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: DownloadAction(source: .timeline)), const ActionColumnButton(action: DeleteAction(source: .timeline)), const ActionColumnButton(action: EditDateTimeAction(source: .timeline)), const ActionColumnButton(action: EditLocationAction(source: .timeline)), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart index 154ba29fa1..ea213fbdc7 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; +import 'package:immich_mobile/presentation/actions/download.action.dart'; import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart'; import 'package:immich_mobile/presentation/actions/edit_location.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; @@ -14,7 +14,6 @@ import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/actions/share.action.dart'; import 'package:immich_mobile/presentation/actions/share_link.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; @@ -81,7 +80,7 @@ class FavoriteBottomSheet extends ConsumerWidget { const ActionColumnButton(action: ShareLinkAction(source: .timeline)), const ActionColumnButton(action: FavoriteAction(source: .timeline)), const ActionColumnButton(action: ArchiveAction(source: .timeline)), - if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: DownloadAction(source: .timeline)), const ActionColumnButton(action: DeleteAction(source: .timeline)), const ActionColumnButton(action: EditDateTimeAction(source: .timeline)), const ActionColumnButton(action: EditLocationAction(source: .timeline)), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart index e21dedc1e3..2ad037f2ad 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart @@ -6,6 +6,7 @@ import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; +import 'package:immich_mobile/presentation/actions/download.action.dart'; import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart'; import 'package:immich_mobile/presentation/actions/edit_location.action.dart'; import 'package:immich_mobile/presentation/actions/favorite.action.dart'; @@ -13,13 +14,11 @@ import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/actions/share.action.dart'; import 'package:immich_mobile/presentation/actions/share_link.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/bulk_tag_assets_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; +import 'package:immich_mobile/presentation/actions/tag.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -48,9 +47,6 @@ class _GeneralBottomSheetState extends ConsumerState { @override Widget build(BuildContext context) { final multiselect = ref.watch(multiSelectProvider); - final tagsEnabled = ref.watch( - userMetadataPreferencesProvider.select((value) => value.valueOrNull?.tagsEnabled ?? false), - ); Future addToAlbum(RemoteAlbum album) async { final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.timeline, album); @@ -86,10 +82,10 @@ class _GeneralBottomSheetState extends ConsumerState { const ActionColumnButton(action: ShareAction(source: .timeline)), if (multiselect.hasRemote) ...[ const ActionColumnButton(action: ShareLinkAction(source: .timeline)), - if (multiselect.onlyRemote) const DownloadActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: DownloadAction(source: .timeline)), const ActionColumnButton(action: FavoriteAction(source: .timeline)), const ActionColumnButton(action: ArchiveAction(source: .timeline)), - if (tagsEnabled) const BulkTagAssetsActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: TagAction(source: .timeline)), const ActionColumnButton(action: EditDateTimeAction(source: .timeline)), const ActionColumnButton(action: EditLocationAction(source: .timeline)), const ActionColumnButton(action: LockAction(source: .timeline)), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart index 3a9d35b46d..1d3d5b0d26 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; +import 'package:immich_mobile/presentation/actions/download.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/actions/share.action.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; class LockedFolderBottomSheet extends ConsumerWidget { @@ -19,7 +18,7 @@ class LockedFolderBottomSheet extends ConsumerWidget { shouldCloseOnMinExtent: false, actions: [ ActionColumnButton(action: ShareAction(source: .timeline)), - DownloadActionButton(source: ActionSource.timeline), + ActionColumnButton(action: DownloadAction(source: .timeline)), ActionColumnButton(action: DeleteAction(source: .timeline)), ActionColumnButton(action: LockAction(source: .timeline)), ], diff --git a/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart index 84d6b4478b..8edc2e04e6 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart @@ -4,8 +4,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/download.action.dart'; import 'package:immich_mobile/presentation/actions/share.action.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; @@ -66,7 +66,7 @@ class _PartnerDetailBottomSheetState extends ConsumerState const ActionColumnButton(action: ArchiveAction(source: .timeline)), const ActionColumnButton(action: FavoriteAction(source: .timeline)), ], - const DownloadActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: DownloadAction(source: .timeline)), if (ownsAlbum) ...[ const ActionColumnButton(action: DeleteAction(source: .timeline)), const ActionColumnButton(action: EditDateTimeAction(source: .timeline)), diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index c10ac37b22..919836737f 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -10,9 +10,7 @@ import 'package:immich_mobile/domain/services/remote_album.service.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/tag.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/action.service.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart'; @@ -57,11 +55,6 @@ class ActionNotifier extends Notifier { return _getAssets(source).whereType().toIds().toList(growable: false); } - List _getOwnedRemoteIdsForSource(ActionSource source) { - final ownerId = ref.read(currentUserProvider)?.id; - return _getAssets(source).whereType().ownedAssets(ownerId).toIds().toList(growable: false); - } - Set _getAssets(ActionSource source) { return switch (source) { ActionSource.timeline => ref.read(multiSelectProvider).selectedAssets, @@ -102,23 +95,6 @@ class ActionNotifier extends Notifier { } } - Future tagAssets(ActionSource source, BuildContext context) async { - final ids = _getOwnedRemoteIdsForSource(source); - try { - final count = await _service.tagAssets(ids, context); - if (count == null) { - return null; - } - - ref.invalidate(tagProvider); - return ActionResult(count: count, success: true); - } catch (error, stack) { - _logger.severe('Failed to tag assets', error, stack); - ref.invalidate(tagProvider); - return ActionResult(count: ids.length, success: false, error: error.toString()); - } - } - Future addToAlbum(ActionSource source, RemoteAlbum album) async { final selected = _getAssets(source).toList(growable: false); if (selected.isEmpty) { @@ -201,18 +177,6 @@ class ActionNotifier extends Notifier { } } - Future downloadAll(ActionSource source) async { - final assets = _getAssets(source).whereType().toList(growable: false); - try { - final didEnqueue = await _service.downloadAll(assets); - final enqueueCount = didEnqueue.where((e) => e).length; - return ActionResult(count: enqueueCount, success: true); - } catch (error, stack) { - _logger.severe('Failed to download assets', error, stack); - return ActionResult(count: assets.length, success: false, error: error.toString()); - } - } - Future upload( ActionSource source, { List? assets, @@ -297,11 +261,4 @@ class ActionNotifier extends Notifier { extension on Iterable { Iterable toIds() => map((e) => e.id); - - Iterable ownedAssets(String? ownerId) { - if (ownerId == null) { - return const []; - } - return whereType().where((a) => a.ownerId == ownerId); - } } diff --git a/mobile/lib/services/action.service.dart b/mobile/lib/services/action.service.dart index 90e7978c97..ec9b7cd446 100644 --- a/mobile/lib/services/action.service.dart +++ b/mobile/lib/services/action.service.dart @@ -1,36 +1,19 @@ import 'dart:async'; -import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/services/tag.service.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/repositories/asset_api.repository.dart'; -import 'package:immich_mobile/repositories/download.repository.dart'; -import 'package:immich_mobile/widgets/common/tag_picker.dart'; final actionServiceProvider = Provider( - (ref) => ActionService( - ref.watch(assetApiRepositoryProvider), - ref.watch(remoteAssetRepositoryProvider), - ref.watch(downloadRepositoryProvider), - ref.watch(tagServiceProvider), - ), + (ref) => ActionService(ref.watch(assetApiRepositoryProvider), ref.watch(remoteAssetRepositoryProvider)), ); class ActionService { final AssetApiRepository _assetApiRepository; final RemoteAssetRepository _remoteAssetRepository; - final DownloadRepository _downloadRepository; - final TagService _tagService; - const ActionService( - this._assetApiRepository, - this._remoteAssetRepository, - this._downloadRepository, - this._tagService, - ); + const ActionService(this._assetApiRepository, this._remoteAssetRepository); Future emptyTrash(String userId) async { final count = await _assetApiRepository.emptyTrash(); @@ -59,28 +42,4 @@ class ActionService { return true; } - - Future tagAssets(List remoteIds, BuildContext context) async { - final tagResults = await showTagPickerModal(context: context); - if (tagResults == null) { - // user cancelled - return null; - } - - final selectedTagIds = Set.from(tagResults.$1); - final selectedNewTagValues = tagResults.$2; - - if (selectedNewTagValues.isNotEmpty) { - final upsertedTags = await _tagService.upsertTags(selectedNewTagValues.toList()); - selectedTagIds.addAll(upsertedTags.map((t) => t.id)); - } - if (selectedTagIds.isEmpty) { - return 0; - } - return _tagService.bulkTagAssets(remoteIds, selectedTagIds.toList()); - } - - Future> downloadAll(List assets) { - return _downloadRepository.downloadAllAssets(assets); - } } diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index 7a416a4441..e3a2876d32 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -12,6 +12,7 @@ import 'package:immich_mobile/presentation/actions/archive.action.dart'; import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; import 'package:immich_mobile/presentation/actions/cast.action.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; +import 'package:immich_mobile/presentation/actions/download.action.dart'; import 'package:immich_mobile/presentation/actions/lock.action.dart'; import 'package:immich_mobile/presentation/actions/open_in_browser.action.dart'; import 'package:immich_mobile/presentation/actions/remove_from_album.action.dart'; @@ -24,7 +25,6 @@ import 'package:immich_mobile/presentation/actions/similar_photos.action.dart'; import 'package:immich_mobile/presentation/actions/slideshow.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -178,7 +178,7 @@ enum ActionButtonType { ActionButtonType.slideshow => const ActionMenuItem(action: SlideshowAction()), ActionButtonType.archive || ActionButtonType.unarchive => ActionMenuItem(action: ArchiveAction(source: context.source)), - ActionButtonType.download => DownloadActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), + ActionButtonType.download => ActionMenuItem(action: DownloadAction(source: context.source)), ActionButtonType.restoreTrash => ActionMenuItem(action: RestoreAction(source: context.source)), ActionButtonType.delete => ActionMenuItem(action: DeleteAction(source: context.source)), ActionButtonType.moveToLockFolder || diff --git a/mobile/test/services/action.service_test.dart b/mobile/test/services/action.service_test.dart index 389d4200da..2d90e24503 100644 --- a/mobile/test/services/action.service_test.dart +++ b/mobile/test/services/action.service_test.dart @@ -17,8 +17,6 @@ void main() { late MockAssetApiRepository assetApiRepository; late MockRemoteAssetRepository remoteAssetRepository; - late MockDownloadRepository downloadRepository; - late MockTagService tagService; late Drift db; @@ -39,10 +37,8 @@ void main() { setUp(() { assetApiRepository = MockAssetApiRepository(); remoteAssetRepository = MockRemoteAssetRepository(); - downloadRepository = MockDownloadRepository(); - tagService = MockTagService(); - sut = ActionService(assetApiRepository, remoteAssetRepository, downloadRepository, tagService); + sut = ActionService(assetApiRepository, remoteAssetRepository); }); tearDown(() async { diff --git a/mobile/test/unit/presentation/actions/download_tag_action_test.dart b/mobile/test/unit/presentation/actions/download_tag_action_test.dart new file mode 100644 index 0000000000..382b12ec83 --- /dev/null +++ b/mobile/test/unit/presentation/actions/download_tag_action_test.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/tag.model.dart'; +import 'package:immich_mobile/domain/services/tag.service.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/download.action.dart'; +import 'package:immich_mobile/presentation/actions/tag.action.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/user_metadata.provider.dart'; +import 'package:immich_mobile/repositories/download.repository.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../repository.mocks.dart'; +import '../../factories/local_asset_factory.dart'; +import '../../factories/remote_asset_factory.dart'; +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + late MockTagService tagService; + + setUp(() async { + context = await PresentationContext.create(); + tagService = context.service.tag.service; + }); + + tearDown(() { + context.dispose(); + }); + + RemoteAsset owned() => RemoteAssetFactory.create(ownerId: context.currentUser.id); + + group('DownloadAction', () { + Future pumpDownload(WidgetTester tester, Set selection) => tester.pumpTestAction( + context, + const DownloadAction(source: .timeline), + overrides: [ + ...context.selected(selection), + downloadRepositoryProvider.overrideWithValue(context.repository.download.repo), + backgroundSyncProvider.overrideWithValue(context.service.backgroundSync), + ], + ); + + Future settleDownload(WidgetTester tester) async { + await tester.pump(); + await tester.pump(const .new(seconds: 1)); + await tester.pumpAndSettle(); + } + + testWidgets('downloads every selected remote asset', (tester) async { + final asset = owned(); + + await pumpDownload(tester, {asset}); + await settleDownload(tester); + + verify(() => context.repository.download.repo.downloadAllAssets([asset])).called(1); + }); + + testWidgets('ignores local-only assets, which are already on the device', (tester) async { + final remote = owned(); + + await pumpDownload(tester, {remote, LocalAssetFactory.create()}); + await settleDownload(tester); + + verify(() => context.repository.download.repo.downloadAllAssets([remote])).called(1); + }); + + testWidgets('is hidden when nothing remote is selected', (tester) async { + await tester.pumpTestWidget( + context, + const ActionIconButton(action: DownloadAction(source: .timeline)), + overrides: context.selected({LocalAssetFactory.create()}), + ); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); + + group('TagAction', () { + late BuildContext actionContext; + late WidgetRef actionRef; + + Future pumpTag(WidgetTester tester, Set selection) => tester.pumpTestWidget( + context, + Consumer( + builder: (widgetContext, ref, _) { + actionContext = widgetContext; + actionRef = ref; + return const ActionIconButton(action: TagAction(source: .timeline)); + }, + ), + overrides: [ + ...context.selected(selection), + toastServiceProvider.overrideWithValue(context.service.toast), + tagServiceProvider.overrideWithValue(tagService), + userMetadataPreferencesProvider.overrideWith((ref) async => const .new(tagsEnabled: true)), + ], + ); + + Future applyTags(List assetIds, {Set selected = const {}, Set created = const {}}) => + tagAssets(actionContext, actionRef, assetIds, selected: selected, created: created); + + testWidgets('offers tagging for an owned asset', (tester) async { + await pumpTag(tester, {owned()}); + + expect(find.byType(ImmichIconButton), findsOneWidget); + }); + + testWidgets('is hidden without any owned asset', (tester) async { + await pumpTag(tester, {RemoteAssetFactory.create()}); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('applies the picked tags and reports the count', (tester) async { + final asset = owned(); + when(() => tagService.bulkTagAssets(any(), any())).thenAnswer((_) async => 1); + + await pumpTag(tester, {asset}); + await applyTags([asset.id], selected: {'tag-1'}); + await tester.pumpAndSettle(); + + verify(() => tagService.bulkTagAssets([asset.id], ['tag-1'])).called(1); + + final message = verify(() => context.service.toast.success(captureAny())).captured.single as String; + expect(message, StaticTranslations.instance.tagged_assets(count: 1)); + }); + + testWidgets('creates new tags first and applies them alongside the picked ones', (tester) async { + final asset = owned(); + when(() => tagService.upsertTags(any())).thenAnswer((_) async => [const Tag(id: 'made-1', value: 'brand new')]); + when(() => tagService.bulkTagAssets(any(), any())).thenAnswer((_) async => 1); + + await pumpTag(tester, {asset}); + await applyTags([asset.id], selected: {'tag-1'}, created: {'brand new'}); + await tester.pumpAndSettle(); + + verify(() => tagService.upsertTags(['brand new'])).called(1); + final tagIds = verify(() => tagService.bulkTagAssets([asset.id], captureAny())).captured.single as List; + expect(tagIds, containsAll(['tag-1', 'made-1'])); + }); + + testWidgets('does nothing when no tag was chosen or created', (tester) async { + final asset = owned(); + + await pumpTag(tester, {asset}); + await applyTags([asset.id]); + await tester.pumpAndSettle(); + + verifyNever(() => tagService.bulkTagAssets(any(), any())); + verifyNever(() => context.service.toast.success(any())); + }); + }); +} From 3d42c1424c2bb446f95aba67c58c8e6f445a460c Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Sat, 1 Aug 2026 08:06:43 +0530 Subject: [PATCH 42/81] refactor: mobile upload action (#29949) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../presentation/actions/upload.action.dart | 145 ++++++++++++ .../upload_action_button.widget.dart | 152 ------------- .../asset_viewer/bottom_bar.widget.dart | 5 +- .../general_bottom_sheet.widget.dart | 4 +- .../local_album_bottom_sheet.widget.dart | 4 +- mobile/lib/utils/action_button.utils.dart | 6 +- .../actions/upload_action_test.dart | 213 ++++++++++++++++++ 7 files changed, 368 insertions(+), 161 deletions(-) create mode 100644 mobile/lib/presentation/actions/upload.action.dart delete mode 100644 mobile/lib/presentation/widgets/action_buttons/upload_action_button.widget.dart create mode 100644 mobile/test/unit/presentation/actions/upload_action_test.dart diff --git a/mobile/lib/presentation/actions/upload.action.dart b/mobile/lib/presentation/actions/upload.action.dart new file mode 100644 index 0000000000..ceb35c8786 --- /dev/null +++ b/mobile/lib/presentation/actions/upload.action.dart @@ -0,0 +1,145 @@ +import 'dart:async'; + +import 'package:flutter/material.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/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.dart'; +import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/services/foreground_upload.service.dart'; +import 'package:immich_mobile/utils/error_handler.dart'; +import 'package:immich_ui/immich_ui.dart'; + +final _stateProvider = Provider.family.autoDispose?, ActionSource>((ref, source) { + final assets = ref.watch(assetsActionProvider(source)); + final local = assets.backedUp(isBackedUp: false).local().toList(growable: false); + return local.isEmpty ? null : local; +}); + +class UploadAction extends AssetActionBuilder { + final bool showProgress; + + const UploadAction({required super.source, this.showProgress = false}); + + @override + ActionItem? create(BuildContext context, WidgetRef ref) { + final assets = ref.watch(_stateProvider(source)); + if (assets == null) { + return null; + } + + return .new(icon: Icons.backup_outlined, label: context.t.upload, onAction: () => _upload(context, ref, assets)); + } + + Future _upload(BuildContext context, WidgetRef ref, List assets) async { + try { + if (!showProgress) { + await uploadAssets(context, ref, assets); + return; + } + + // The dialog is not awaited: it stays up while the upload runs and is + // dismissed below, unless the user cancelled it themselves first + var isDialogOpen = true; + unawaited( + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => const _UploadProgressDialog(), + ).whenComplete(() => isDialogOpen = false), + ); + + await uploadAssets(context, ref, assets); + + if (isDialogOpen && context.mounted) { + Navigator.of(context, rootNavigator: true).pop(); + } + } catch (error, stack) { + handleError(error, stack: stack, description: "Failed to upload the assets"); + } + } +} + +@visibleForTesting +Future uploadAssets(BuildContext context, WidgetRef ref, List assets) async { + final progress = ref.read(assetUploadProgressProvider.notifier); + final uploads = ref.read(foregroundUploadServiceProvider); + final toastService = ref.read(toastServiceProvider); + final errorMessage = context.t.scaffold_body_error_occurred; + + final cancelToken = Completer(); + ref.read(manualUploadCancelTokenProvider.notifier).state = cancelToken; + + final uploaded = {}; + final failed = {}; + for (final asset in assets) { + progress.setProgress(asset.id, 0.0); + } + + try { + await uploads.uploadManual( + assets, + cancelToken: cancelToken, + callbacks: UploadCallbacks( + onProgress: (id, _, bytes, total) => progress.setProgress(id, total > 0 ? bytes / total : 0.0), + onSuccess: (id, _) { + uploaded.add(id); + progress.remove(id); + }, + onError: (id, _) { + failed.add(id); + progress.setError(id); + }, + ), + ); + } finally { + ref.read(manualUploadCancelTokenProvider.notifier).state = null; + } + + final uploadedCount = uploaded.difference(failed).length; + if (!cancelToken.isCompleted && (uploadedCount != assets.length || failed.isNotEmpty)) { + toastService.error(errorMessage); + } + + unawaited(Future.delayed(const Duration(seconds: 2), progress.clear)); +} + +class _UploadProgressDialog extends ConsumerWidget { + const _UploadProgressDialog(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final progressMap = ref.watch(assetUploadProgressProvider); + + final values = progressMap.values.where((value) => value >= 0).toList(growable: false); + final progress = values.isEmpty ? 0.0 : values.reduce((a, b) => a + b) / values.length; + final hasError = progressMap.values.any((value) => value < 0); + + return AlertDialog( + title: Text(context.t.uploading), + content: Column( + mainAxisSize: .min, + children: [ + if (hasError) + const Icon(Icons.error_outline, color: Colors.red, size: 48) + else + CircularProgressIndicator(value: progress > 0 ? progress : null), + const SizedBox(height: 16), + Text(hasError ? context.t.scaffold_body_error_occurred : '${(progress * 100).toInt()}%'), + ], + ), + actions: [ + ImmichTextButton( + onPressed: () { + ref.read(manualUploadCancelTokenProvider)?.complete(); + ref.read(manualUploadCancelTokenProvider.notifier).state = null; + Navigator.of(context, rootNavigator: true).pop(); + }, + labelText: context.t.cancel, + ), + ], + ); + } +} diff --git a/mobile/lib/presentation/widgets/action_buttons/upload_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/upload_action_button.widget.dart deleted file mode 100644 index 1d09ad23a8..0000000000 --- a/mobile/lib/presentation/widgets/action_buttons/upload_action_button.widget.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:flutter/material.dart'; -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/extensions/translate_extensions.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; -import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; -import 'package:immich_mobile/providers/view_intent/view_intent_file_path.provider.dart'; -import 'package:immich_mobile/services/foreground_upload.service.dart'; -import 'package:immich_mobile/services/view_intent.service.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_ui/immich_ui.dart'; - -class UploadActionButton extends ConsumerWidget { - final ActionSource source; - final bool iconOnly; - final bool menuItem; - - const UploadActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false}); - - Future _onTap(BuildContext context, WidgetRef ref) async { - if (!context.mounted) { - return; - } - - final isTimeline = source == ActionSource.timeline; - final viewerIntentFilePath = source == ActionSource.viewer ? ref.read(viewIntentFilePathProvider) : null; - List? assets; - var isUploadDialogOpen = false; - var wasUploadCancelled = false; - Future? uploadDialogFuture; - - if (source == ActionSource.timeline) { - assets = ref.read(multiSelectProvider).selectedAssets.whereType().toList(); - if (assets.isEmpty) { - return; - } - ref.read(multiSelectProvider.notifier).reset(); - } else { - isUploadDialogOpen = true; - uploadDialogFuture = - showDialog( - context: context, - barrierDismissible: false, - builder: (dialogContext) => _UploadProgressDialog( - onCancel: () { - wasUploadCancelled = true; - }, - ), - ).whenComplete(() { - isUploadDialogOpen = false; - }); - unawaited(uploadDialogFuture); - } - - var success = false; - if (!isTimeline && viewerIntentFilePath != null) { - final viewIntentService = ref.read(viewIntentServiceProvider); - viewIntentService.markUploadActive(viewerIntentFilePath); - var hasError = false; - try { - await ref - .read(foregroundUploadServiceProvider) - .uploadShareIntent( - [File(viewerIntentFilePath)], - onError: (_, _) { - hasError = true; - }, - ); - } finally { - await viewIntentService.markUploadInactive(viewerIntentFilePath); - } - success = !hasError; - } else { - final result = await ref.read(actionProvider.notifier).upload(source, assets: assets); - success = result.success; - } - - if (!isTimeline && context.mounted && isUploadDialogOpen) { - Navigator.of(context, rootNavigator: true).pop(); - } - - if (context.mounted && !success && !wasUploadCancelled) { - ImmichToast.show( - context: context, - msg: 'scaffold_body_error_occurred'.t(context: context), - gravity: ToastGravity.BOTTOM, - toastType: ToastType.error, - ); - } - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - return BaseActionButton( - iconData: Icons.backup_outlined, - label: "upload".t(context: context), - iconOnly: iconOnly, - menuItem: menuItem, - onPressed: () => _onTap(context, ref), - ); - } -} - -class _UploadProgressDialog extends ConsumerWidget { - final VoidCallback onCancel; - - const _UploadProgressDialog({required this.onCancel}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final progressMap = ref.watch(assetUploadProgressProvider); - - // Calculate overall progress from all assets - final values = progressMap.values.where((v) => v >= 0).toList(); - final progress = values.isEmpty ? 0.0 : values.reduce((a, b) => a + b) / values.length; - final hasError = progressMap.values.any((v) => v < 0); - final percentage = (progress * 100).toInt(); - - return AlertDialog( - title: Text('uploading'.t(context: context)), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (hasError) - const Icon(Icons.error_outline, color: Colors.red, size: 48) - else - CircularProgressIndicator(value: progress > 0 ? progress : null), - const SizedBox(height: 16), - Text(hasError ? 'Error' : '$percentage%'), - ], - ), - actions: [ - ImmichTextButton( - onPressed: () { - ref.read(manualUploadCancelTokenProvider)?.complete(); - ref.read(manualUploadCancelTokenProvider.notifier).state = null; - onCancel(); - Navigator.of(context, rootNavigator: true).pop(); - }, - labelText: 'cancel'.t(context: context), - ), - ], - ); - } -} diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart index c1f6ec8b47..fb82b61d62 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/services/timeline.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/presentation/actions/action.dart'; @@ -8,8 +7,8 @@ import 'package:immich_mobile/presentation/actions/delete.action.dart'; import 'package:immich_mobile/presentation/actions/edit_asset.action.dart'; import 'package:immich_mobile/presentation/actions/restore.action.dart'; import 'package:immich_mobile/presentation/actions/share.action.dart'; +import 'package:immich_mobile/presentation/actions/upload.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/ocr_toggle_button.widget.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; @@ -49,7 +48,7 @@ class ViewerBottomBar extends ConsumerWidget { if (!isInLockedView) ...[ if (!isInTrash) ...[ - if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer), + ..._actionColumnButtons(context, ref, const [UploadAction(source: .viewer, showProgress: true)]), ..._actionColumnButtons(context, ref, const [EditAssetAction(source: .viewer)]), if (asset.hasRemote) AddActionButton(originalTheme: originalTheme), ], diff --git a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart index 2ad037f2ad..df0fca336f 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart @@ -15,7 +15,7 @@ import 'package:immich_mobile/presentation/actions/share.action.dart'; import 'package:immich_mobile/presentation/actions/share_link.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/actions/tag.action.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; +import 'package:immich_mobile/presentation/actions/upload.action.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; @@ -93,7 +93,7 @@ class _GeneralBottomSheetState extends ConsumerState { ], const ActionColumnButton(action: DeleteAction(source: .timeline)), const ActionColumnButton(action: CleanupLocalAction(source: .timeline)), - if (multiselect.onlyLocal) const UploadActionButton(source: ActionSource.timeline), + const ActionColumnButton(action: UploadAction(source: .timeline)), ], slivers: [ const AddToAlbumHeader(), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart index 3e8d982a21..80a5b43686 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart @@ -6,7 +6,7 @@ import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; import 'package:immich_mobile/presentation/actions/share.action.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; +import 'package:immich_mobile/presentation/actions/upload.action.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; @@ -69,7 +69,7 @@ class _LocalAlbumBottomSheetState extends ConsumerState { ActionColumnButton(action: ShareAction(source: .timeline)), ActionColumnButton(action: DeleteAction(source: .timeline)), ActionColumnButton(action: CleanupLocalAction(source: .timeline)), - UploadActionButton(source: ActionSource.timeline), + ActionColumnButton(action: UploadAction(source: .timeline)), ], slivers: [ const AddToAlbumHeader(), diff --git a/mobile/lib/utils/action_button.utils.dart b/mobile/lib/utils/action_button.utils.dart index e3a2876d32..28a9442dc7 100644 --- a/mobile/lib/utils/action_button.utils.dart +++ b/mobile/lib/utils/action_button.utils.dart @@ -24,9 +24,9 @@ import 'package:immich_mobile/presentation/actions/share_link.action.dart'; import 'package:immich_mobile/presentation/actions/similar_photos.action.dart'; import 'package:immich_mobile/presentation/actions/slideshow.action.dart'; import 'package:immich_mobile/presentation/actions/stack.action.dart'; +import 'package:immich_mobile/presentation/actions/upload.action.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart'; -import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/routing/router.dart'; class ActionButtonContext { @@ -184,7 +184,9 @@ enum ActionButtonType { ActionButtonType.moveToLockFolder || ActionButtonType.removeFromLockFolder => ActionMenuItem(action: LockAction(source: context.source)), ActionButtonType.deleteLocal => ActionMenuItem(action: CleanupLocalAction(source: context.source)), - ActionButtonType.upload => UploadActionButton(source: context.source, iconOnly: iconOnly, menuItem: menuItem), + ActionButtonType.upload => ActionMenuItem( + action: UploadAction(source: context.source, showProgress: context.source == ActionSource.viewer), + ), ActionButtonType.removeFromAlbum => ActionMenuItem( action: RemoveFromAlbumAction(source: context.source, albumId: context.currentAlbum!.id), ), diff --git a/mobile/test/unit/presentation/actions/upload_action_test.dart b/mobile/test/unit/presentation/actions/upload_action_test.dart new file mode 100644 index 0000000000..2489f3100c --- /dev/null +++ b/mobile/test/unit/presentation/actions/upload_action_test.dart @@ -0,0 +1,213 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/presentation/actions/action.widget.dart'; +import 'package:immich_mobile/presentation/actions/upload.action.dart'; +import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/toast.provider.dart'; +import 'package:immich_mobile/services/foreground_upload.service.dart'; +import 'package:immich_ui/immich_ui.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../service.mocks.dart'; +import '../../factories/local_asset_factory.dart'; +import '../../factories/remote_asset_factory.dart'; +import '../presentation_context.dart'; + +void main() { + late PresentationContext context; + late MockForegroundUploadService uploadService; + + setUp(() async { + context = await PresentationContext.create(); + uploadService = context.service.upload; + }); + + tearDown(() { + context.dispose(); + }); + + List uploadOverrides() => [ + foregroundUploadServiceProvider.overrideWithValue(uploadService), + toastServiceProvider.overrideWithValue(context.service.toast), + ]; + + Future pumpUpload(WidgetTester tester, Set selection, {bool showProgress = false}) => + tester.pumpTestWidget( + context, + ActionIconButton( + action: UploadAction(source: .timeline, showProgress: showProgress), + ), + overrides: [...context.selected(selection), ...uploadOverrides()], + ); + + Future settleUpload(WidgetTester tester) async { + await tester.pump(); + await tester.pump(const .new(seconds: 2)); + await tester.pumpAndSettle(); + } + + void answerUploadWith({Set succeeded = const {}, Set failed = const {}, Completer? until}) { + when( + () => uploadService.uploadManual( + any(), + cancelToken: any(named: 'cancelToken'), + callbacks: any(named: 'callbacks'), + ), + ).thenAnswer((invocation) async { + if (until != null) { + await until.future; + } + final callbacks = invocation.namedArguments[#callbacks] as UploadCallbacks; + for (final id in succeeded) { + callbacks.onSuccess?.call(id, id); + } + for (final id in failed) { + callbacks.onError?.call(id, 'boom'); + } + }); + } + + group('UploadAction', () { + testWidgets('uploads the selected local assets', (tester) async { + final asset = LocalAssetFactory.create(); + answerUploadWith(succeeded: {asset.id}); + + await pumpUpload(tester, {asset}); + await tester.tap(find.byType(ImmichIconButton)); + await settleUpload(tester); + + final uploaded = + verify( + () => uploadService.uploadManual( + captureAny(), + cancelToken: any(named: 'cancelToken'), + callbacks: any(named: 'callbacks'), + ), + ).captured.single + as List; + expect(uploaded.map((a) => a.id), [asset.id]); + }); + + testWidgets('ignores assets that are already backed up', (tester) async { + final notBackedUp = LocalAssetFactory.create(); + answerUploadWith(succeeded: {notBackedUp.id}); + + await pumpUpload(tester, {notBackedUp, LocalAssetFactory.create(remoteId: 'already-there')}); + await tester.tap(find.byType(ImmichIconButton)); + await settleUpload(tester); + + final uploaded = + verify( + () => uploadService.uploadManual( + captureAny(), + cancelToken: any(named: 'cancelToken'), + callbacks: any(named: 'callbacks'), + ), + ).captured.single + as List; + expect(uploaded.map((a) => a.id), [notBackedUp.id]); + }); + + testWidgets('reports an error when an asset fails to upload', (tester) async { + final asset = LocalAssetFactory.create(); + answerUploadWith(failed: {asset.id}); + + await pumpUpload(tester, {asset}); + await tester.tap(find.byType(ImmichIconButton)); + await settleUpload(tester); + + final message = verify(() => context.service.toast.error(captureAny())).captured.single as String; + expect(message, StaticTranslations.instance.scaffold_body_error_occurred); + }); + + testWidgets('treats a cancelled upload as deliberate, not a failure', (tester) async { + final asset = LocalAssetFactory.create(); + when( + () => uploadService.uploadManual( + any(), + cancelToken: any(named: 'cancelToken'), + callbacks: any(named: 'callbacks'), + ), + ).thenAnswer((invocation) async { + (invocation.namedArguments[#cancelToken] as Completer).complete(); + }); + + await pumpUpload(tester, {asset}); + await tester.tap(find.byType(ImmichIconButton)); + await settleUpload(tester); + + verifyNever(() => context.service.toast.error(any())); + }); + + testWidgets('shows the progress dialog while uploading and closes it after', (tester) async { + final asset = LocalAssetFactory.create(); + final uploading = Completer(); + answerUploadWith(succeeded: {asset.id}, until: uploading); + + await pumpUpload(tester, {asset}, showProgress: true); + await tester.tap(find.byType(ImmichIconButton)); + await tester.pump(); + + expect(find.text(StaticTranslations.instance.uploading), findsOneWidget); + + uploading.complete(); + await settleUpload(tester); + + expect(find.text(StaticTranslations.instance.uploading), findsNothing); + }); + + testWidgets('shows no dialog when not asked to', (tester) async { + final asset = LocalAssetFactory.create(); + answerUploadWith(succeeded: {asset.id}); + + await pumpUpload(tester, {asset}); + await tester.tap(find.byType(ImmichIconButton)); + await tester.pump(); + + expect(find.text(StaticTranslations.instance.uploading), findsNothing); + await settleUpload(tester); + }); + + testWidgets('is hidden for a remote asset, which has nothing to upload', (tester) async { + await pumpUpload(tester, {RemoteAssetFactory.create()}); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + + testWidgets('is hidden when every local asset is already backed up', (tester) async { + await pumpUpload(tester, {LocalAssetFactory.create(remoteId: 'already-there')}); + + expect(find.byType(ImmichIconButton), findsNothing); + }); + }); + + group('uploadAssets', () { + testWidgets('clears the tracked progress once the upload settles', (tester) async { + final asset = LocalAssetFactory.create(); + answerUploadWith(succeeded: {asset.id}); + + late WidgetRef capturedRef; + await tester.pumpTestWidget( + context, + Consumer( + builder: (_, ref, _) { + capturedRef = ref; + return const SizedBox.shrink(); + }, + ), + overrides: uploadOverrides(), + ); + + await uploadAssets(tester.element(find.byType(SizedBox)), capturedRef, [asset]); + await settleUpload(tester); + + expect(capturedRef.read(assetUploadProgressProvider), isEmpty); + expect(capturedRef.read(manualUploadCancelTokenProvider), isNull); + }); + }); +} From cafd6c7c0f12add9b2c0f06a0cc1c26ef26e2756 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Sat, 1 Aug 2026 08:06:44 +0530 Subject: [PATCH 43/81] refactor: cleanup actions & action tests (#30265) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../repositories/settings.repository.dart | 10 ++++++ .../asset_viewer/bottom_bar.widget.dart | 9 ++--- .../archive_bottom_sheet.widget.dart | 29 +++++++-------- .../favorite_bottom_sheet.widget.dart | 26 +++++++------- .../general_bottom_sheet.widget.dart | 36 +++++++++---------- .../local_album_bottom_sheet.widget.dart | 10 +++--- .../locked_folder_bottom_sheet.widget.dart | 10 +++--- .../partner_detail_bottom_sheet.widget.dart | 6 ++-- .../remote_album_bottom_sheet.widget.dart | 34 ++++++++---------- .../trash_bottom_sheet.widget.dart | 6 ++-- .../local_album_repository_test.dart | 4 +++ .../asset_viewer_system_ui_test.dart | 4 +++ mobile/test/unit/mocks.dart | 4 +++ .../actions/album_action_test.dart | 4 +-- .../actions/archive_action_test.dart | 4 +-- .../actions/asset_debug_action_test.dart | 4 +-- .../actions/cast_action_test.dart | 4 +-- .../actions/delete_action_test.dart | 2 +- .../actions/download_tag_action_test.dart | 4 +-- .../actions/edit_action_test.dart | 4 +-- .../actions/favorite_action_test.dart | 4 +-- .../actions/lock_action_test.dart | 4 +-- .../actions/open_in_browser_action_test.dart | 4 +-- .../actions/partner_action_test.dart | 4 +-- .../actions/restore_action_test.dart | 4 +-- .../actions/share_action_test.dart | 14 ++------ .../actions/stack_action_test.dart | 4 +-- .../actions/upload_action_test.dart | 4 +-- .../unit/presentation/partner_page_test.dart | 2 +- .../presentation/presentation_context.dart | 6 ++-- 30 files changed, 132 insertions(+), 132 deletions(-) diff --git a/mobile/lib/infrastructure/repositories/settings.repository.dart b/mobile/lib/infrastructure/repositories/settings.repository.dart index 7063779336..c5f8fb48d8 100644 --- a/mobile/lib/infrastructure/repositories/settings.repository.dart +++ b/mobile/lib/infrastructure/repositories/settings.repository.dart @@ -1,4 +1,5 @@ import 'package:drift/drift.dart'; +import 'package:flutter/foundation.dart'; import 'package:immich_mobile/domain/models/config/app_config.dart'; import 'package:immich_mobile/domain/models/settings_key.dart'; import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart'; @@ -29,6 +30,15 @@ class SettingsRepository extends CachedKeyValueRepository reset() async { + final instance = _instance; + if (instance != null) { + await instance.clear(SettingsKey.values); + _instance = null; + } + } + @override List get keys => SettingsKey.values; diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart index fb82b61d62..3ed2b3a576 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_bar.widget.dart @@ -43,13 +43,14 @@ class ViewerBottomBar extends ConsumerWidget { final originalTheme = context.themeData; final actions = [ - ..._actionColumnButtons(context, ref, const [RestoreAction(source: .viewer)]), - ..._actionColumnButtons(context, ref, const [ShareAction(source: .viewer)]), + ..._actionColumnButtons(context, ref, const [RestoreAction(source: .viewer), ShareAction(source: .viewer)]), if (!isInLockedView) ...[ if (!isInTrash) ...[ - ..._actionColumnButtons(context, ref, const [UploadAction(source: .viewer, showProgress: true)]), - ..._actionColumnButtons(context, ref, const [EditAssetAction(source: .viewer)]), + ..._actionColumnButtons(context, ref, const [ + UploadAction(source: .viewer, showProgress: true), + EditAssetAction(source: .viewer), + ]), if (asset.hasRemote) AddActionButton(originalTheme: originalTheme), ], ..._actionColumnButtons(context, ref, const [DeleteAction(source: .viewer)]), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart index 032ccda917..3e5edaa149 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/archive_bottom_sheet.widget.dart @@ -17,7 +17,6 @@ import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; class ArchiveBottomSheet extends ConsumerStatefulWidget { @@ -44,8 +43,6 @@ class _ArchiveBottomSheetState extends ConsumerState { @override Widget build(BuildContext context) { - final multiselect = ref.watch(multiSelectProvider); - Future addToAlbum(RemoteAlbum album) async { final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.timeline, album); @@ -75,20 +72,18 @@ class _ArchiveBottomSheetState extends ConsumerState { initialChildSize: 0.25, maxChildSize: 0.85, shouldCloseOnMinExtent: false, - actions: [ - const ActionColumnButton(action: ShareAction(source: .timeline)), - if (multiselect.hasRemote) ...[ - const ActionColumnButton(action: ShareLinkAction(source: .timeline)), - const ActionColumnButton(action: ArchiveAction(source: .timeline)), - const ActionColumnButton(action: FavoriteAction(source: .timeline)), - const ActionColumnButton(action: DownloadAction(source: .timeline)), - const ActionColumnButton(action: DeleteAction(source: .timeline)), - const ActionColumnButton(action: EditDateTimeAction(source: .timeline)), - const ActionColumnButton(action: EditLocationAction(source: .timeline)), - const ActionColumnButton(action: LockAction(source: .timeline)), - const ActionColumnButton(action: StackAction(source: .timeline)), - ], - const ActionColumnButton(action: CleanupLocalAction(source: .timeline)), + actions: const [ + .new(action: ShareAction(source: .timeline)), + .new(action: ShareLinkAction(source: .timeline)), + .new(action: ArchiveAction(source: .timeline)), + .new(action: FavoriteAction(source: .timeline)), + .new(action: DownloadAction(source: .timeline)), + .new(action: DeleteAction(source: .timeline)), + .new(action: EditDateTimeAction(source: .timeline)), + .new(action: EditLocationAction(source: .timeline)), + .new(action: LockAction(source: .timeline)), + .new(action: StackAction(source: .timeline)), + .new(action: CleanupLocalAction(source: .timeline)), ], slivers: [ const AddToAlbumHeader(), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart index ea213fbdc7..7540da4fff 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/favorite_bottom_sheet.widget.dart @@ -74,20 +74,18 @@ class FavoriteBottomSheet extends ConsumerWidget { initialChildSize: 0.4, maxChildSize: 0.7, shouldCloseOnMinExtent: false, - actions: [ - const ActionColumnButton(action: ShareAction(source: .timeline)), - if (multiselect.hasRemote) ...[ - const ActionColumnButton(action: ShareLinkAction(source: .timeline)), - const ActionColumnButton(action: FavoriteAction(source: .timeline)), - const ActionColumnButton(action: ArchiveAction(source: .timeline)), - const ActionColumnButton(action: DownloadAction(source: .timeline)), - const ActionColumnButton(action: DeleteAction(source: .timeline)), - const ActionColumnButton(action: EditDateTimeAction(source: .timeline)), - const ActionColumnButton(action: EditLocationAction(source: .timeline)), - const ActionColumnButton(action: LockAction(source: .timeline)), - const ActionColumnButton(action: StackAction(source: .timeline)), - ], - const ActionColumnButton(action: CleanupLocalAction(source: .timeline)), + actions: const [ + .new(action: ShareAction(source: .timeline)), + .new(action: ShareLinkAction(source: .timeline)), + .new(action: FavoriteAction(source: .timeline)), + .new(action: ArchiveAction(source: .timeline)), + .new(action: DownloadAction(source: .timeline)), + .new(action: DeleteAction(source: .timeline)), + .new(action: EditDateTimeAction(source: .timeline)), + .new(action: EditLocationAction(source: .timeline)), + .new(action: LockAction(source: .timeline)), + .new(action: StackAction(source: .timeline)), + .new(action: CleanupLocalAction(source: .timeline)), ], slivers: multiselect.hasRemote ? [const AddToAlbumHeader(), AlbumSelector(onAlbumSelected: addAssetsToAlbum)] diff --git a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart index df0fca336f..d5407e768f 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/archive.action.dart'; +import 'package:immich_mobile/presentation/actions/asset_debug.action.dart'; import 'package:immich_mobile/presentation/actions/delete.action.dart'; import 'package:immich_mobile/presentation/actions/download.action.dart'; import 'package:immich_mobile/presentation/actions/edit_datetime.action.dart'; @@ -19,7 +20,6 @@ import 'package:immich_mobile/presentation/actions/upload.action.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; class GeneralBottomSheet extends ConsumerStatefulWidget { @@ -46,8 +46,6 @@ class _GeneralBottomSheetState extends ConsumerState { @override Widget build(BuildContext context) { - final multiselect = ref.watch(multiSelectProvider); - Future addToAlbum(RemoteAlbum album) async { final result = await ref.read(actionProvider.notifier).addToAlbum(ActionSource.timeline, album); @@ -77,23 +75,21 @@ class _GeneralBottomSheetState extends ConsumerState { minChildSize: widget.minChildSize, maxChildSize: 0.85, shouldCloseOnMinExtent: false, - actions: [ - const ActionColumnButton(action: FavoriteAction(source: .timeline)), - const ActionColumnButton(action: ShareAction(source: .timeline)), - if (multiselect.hasRemote) ...[ - const ActionColumnButton(action: ShareLinkAction(source: .timeline)), - const ActionColumnButton(action: DownloadAction(source: .timeline)), - const ActionColumnButton(action: FavoriteAction(source: .timeline)), - const ActionColumnButton(action: ArchiveAction(source: .timeline)), - const ActionColumnButton(action: TagAction(source: .timeline)), - const ActionColumnButton(action: EditDateTimeAction(source: .timeline)), - const ActionColumnButton(action: EditLocationAction(source: .timeline)), - const ActionColumnButton(action: LockAction(source: .timeline)), - const ActionColumnButton(action: StackAction(source: .timeline)), - ], - const ActionColumnButton(action: DeleteAction(source: .timeline)), - const ActionColumnButton(action: CleanupLocalAction(source: .timeline)), - const ActionColumnButton(action: UploadAction(source: .timeline)), + actions: const [ + .new(action: AssetDebugAction(source: .timeline)), + .new(action: ShareAction(source: .timeline)), + .new(action: ShareLinkAction(source: .timeline)), + .new(action: DownloadAction(source: .timeline)), + .new(action: DeleteAction(source: .timeline)), + .new(action: FavoriteAction(source: .timeline)), + .new(action: ArchiveAction(source: .timeline)), + .new(action: TagAction(source: .timeline)), + .new(action: EditDateTimeAction(source: .timeline)), + .new(action: EditLocationAction(source: .timeline)), + .new(action: LockAction(source: .timeline)), + .new(action: StackAction(source: .timeline)), + .new(action: CleanupLocalAction(source: .timeline)), + .new(action: UploadAction(source: .timeline)), ], slivers: [ const AddToAlbumHeader(), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart index 80a5b43686..478b37212e 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/local_album_bottom_sheet.widget.dart @@ -65,11 +65,11 @@ class _LocalAlbumBottomSheetState extends ConsumerState { initialChildSize: 0.25, maxChildSize: 0.85, shouldCloseOnMinExtent: false, - actions: const [ - ActionColumnButton(action: ShareAction(source: .timeline)), - ActionColumnButton(action: DeleteAction(source: .timeline)), - ActionColumnButton(action: CleanupLocalAction(source: .timeline)), - ActionColumnButton(action: UploadAction(source: .timeline)), + actions: const [ + .new(action: ShareAction(source: .timeline)), + .new(action: DeleteAction(source: .timeline)), + .new(action: CleanupLocalAction(source: .timeline)), + .new(action: UploadAction(source: .timeline)), ], slivers: [ const AddToAlbumHeader(), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart index 1d3d5b0d26..d7334337ae 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/locked_folder_bottom_sheet.widget.dart @@ -16,11 +16,11 @@ class LockedFolderBottomSheet extends ConsumerWidget { initialChildSize: 0.25, maxChildSize: 0.4, shouldCloseOnMinExtent: false, - actions: [ - ActionColumnButton(action: ShareAction(source: .timeline)), - ActionColumnButton(action: DownloadAction(source: .timeline)), - ActionColumnButton(action: DeleteAction(source: .timeline)), - ActionColumnButton(action: LockAction(source: .timeline)), + actions: [ + .new(action: ShareAction(source: .timeline)), + .new(action: DownloadAction(source: .timeline)), + .new(action: DeleteAction(source: .timeline)), + .new(action: LockAction(source: .timeline)), ], ); } diff --git a/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart index 8edc2e04e6..8a20f26c73 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/partner_detail_bottom_sheet.widget.dart @@ -64,9 +64,9 @@ class _PartnerDetailBottomSheetState extends ConsumerState[ + .new(action: ShareAction(source: .timeline)), + .new(action: DownloadAction(source: .timeline)), ], slivers: [ const AddToAlbumHeader(), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart index aacb3fa1a1..1e51a26811 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/remote_album_bottom_sheet.widget.dart @@ -19,7 +19,6 @@ import 'package:immich_mobile/presentation/actions/stack.action.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; -import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -48,7 +47,6 @@ class _RemoteAlbumBottomSheetState extends ConsumerState @override Widget build(BuildContext context) { - final multiselect = ref.watch(multiSelectProvider); final ownsAlbum = ref.watch(currentUserProvider)?.id == widget.album.ownerId; Future addToAlbum(RemoteAlbum album) async { @@ -85,25 +83,23 @@ class _RemoteAlbumBottomSheetState extends ConsumerState minChildSize: 0.22, maxChildSize: 0.85, shouldCloseOnMinExtent: false, - actions: [ - const ActionColumnButton(action: ShareAction(source: .timeline)), - if (multiselect.hasRemote) ...[ - const ActionColumnButton(action: ShareLinkAction(source: .timeline)), + actions: [ + const .new(action: ShareAction(source: .timeline)), + const .new(action: ShareLinkAction(source: .timeline)), - if (ownsAlbum) ...[ - const ActionColumnButton(action: ArchiveAction(source: .timeline)), - const ActionColumnButton(action: FavoriteAction(source: .timeline)), - ], - const ActionColumnButton(action: DownloadAction(source: .timeline)), - if (ownsAlbum) ...[ - const ActionColumnButton(action: DeleteAction(source: .timeline)), - const ActionColumnButton(action: EditDateTimeAction(source: .timeline)), - const ActionColumnButton(action: EditLocationAction(source: .timeline)), - const ActionColumnButton(action: LockAction(source: .timeline)), - const ActionColumnButton(action: StackAction(source: .timeline)), - ], + if (ownsAlbum) ...const [ + .new(action: ArchiveAction(source: .timeline)), + .new(action: FavoriteAction(source: .timeline)), ], - const ActionColumnButton(action: CleanupLocalAction(source: .timeline)), + const .new(action: DownloadAction(source: .timeline)), + if (ownsAlbum) ...const [ + .new(action: DeleteAction(source: .timeline)), + .new(action: EditDateTimeAction(source: .timeline)), + .new(action: EditLocationAction(source: .timeline)), + .new(action: LockAction(source: .timeline)), + .new(action: StackAction(source: .timeline)), + ], + const .new(action: CleanupLocalAction(source: .timeline)), if (ownsAlbum) ...[ ActionColumnButton( action: RemoveFromAlbumAction(source: .timeline, albumId: widget.album.id), diff --git a/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart index 31a36cb970..1d378f1350 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/trash_bottom_sheet.widget.dart @@ -19,9 +19,9 @@ class TrashBottomBar extends ConsumerWidget { top: false, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ActionColumnButton(action: DeleteAction(source: .timeline)), - ActionColumnButton(action: RestoreAction(source: .timeline)), + children: [ + .new(action: DeleteAction(source: .timeline)), + .new(action: RestoreAction(source: .timeline)), ], ), ), diff --git a/mobile/test/infrastructure/repositories/local_album_repository_test.dart b/mobile/test/infrastructure/repositories/local_album_repository_test.dart index bd4acca483..ada4281e2a 100644 --- a/mobile/test/infrastructure/repositories/local_album_repository_test.dart +++ b/mobile/test/infrastructure/repositories/local_album_repository_test.dart @@ -17,6 +17,10 @@ void main() { mediumFactory = MediumFactory(db); }); + tearDown(() async { + await db.close(); + }); + group('getAll', () { test('sorts albums by backupSelection & isIosSharedAlbum', () async { final localAlbumRepo = mediumFactory.getRepository(); diff --git a/mobile/test/presentation/widgets/asset_viewer/asset_viewer_system_ui_test.dart b/mobile/test/presentation/widgets/asset_viewer/asset_viewer_system_ui_test.dart index 14d876a654..3ec55bb515 100644 --- a/mobile/test/presentation/widgets/asset_viewer/asset_viewer_system_ui_test.dart +++ b/mobile/test/presentation/widgets/asset_viewer/asset_viewer_system_ui_test.dart @@ -39,6 +39,10 @@ void main() { context = await PresentationContext.create(); }); + tearDown(() async { + await context.dispose(); + }); + testWidgets('status bar icons are light while the asset viewer is open in light mode', (tester) async { // Emulate arriving from a light-themed page whose AppBar set dark status // bar icons (the state the viewer is opened from in light mode). diff --git a/mobile/test/unit/mocks.dart b/mobile/test/unit/mocks.dart index 31dfb6f862..1d43198263 100644 --- a/mobile/test/unit/mocks.dart +++ b/mobile/test/unit/mocks.dart @@ -95,6 +95,7 @@ class RepositoryMocks { void _stubAssetMediaRepository() { when(assetMedia.shareAssets).thenAnswer((_) async => 1); + when(assetMedia.getOriginalFilename).thenAnswer((_) async => null); } void _stubDownloadRepository() { @@ -389,6 +390,9 @@ extension type const AssetMediaRepositoryStub(MockAssetMediaRepository api) impl cancelCompleter: any(named: 'cancelCompleter'), onAssetDownloadProgress: any(named: 'onAssetDownloadProgress'), ); + + Future Function() get getOriginalFilename => + () => api.getOriginalFilename(any()); } extension type const DownloadRepositoryStub(MockDownloadRepository repo) implements Stub { diff --git a/mobile/test/unit/presentation/actions/album_action_test.dart b/mobile/test/unit/presentation/actions/album_action_test.dart index 1cf413cbdc..7baa14a4eb 100644 --- a/mobile/test/unit/presentation/actions/album_action_test.dart +++ b/mobile/test/unit/presentation/actions/album_action_test.dart @@ -26,8 +26,8 @@ void main() { albumService = context.service.album.service; }); - tearDown(() { - context.dispose(); + tearDown(() async { + await context.dispose(); }); List withMockToast() => [toastServiceProvider.overrideWithValue(context.service.toast)]; diff --git a/mobile/test/unit/presentation/actions/archive_action_test.dart b/mobile/test/unit/presentation/actions/archive_action_test.dart index 82f015903f..555d6af2ff 100644 --- a/mobile/test/unit/presentation/actions/archive_action_test.dart +++ b/mobile/test/unit/presentation/actions/archive_action_test.dart @@ -20,8 +20,8 @@ void main() { assetService = context.service.asset.service; }); - tearDown(() { - context.dispose(); + tearDown(() async { + await context.dispose(); }); RemoteAsset owned({AssetVisibility visibility = .timeline}) => diff --git a/mobile/test/unit/presentation/actions/asset_debug_action_test.dart b/mobile/test/unit/presentation/actions/asset_debug_action_test.dart index 1644df0396..f545075809 100644 --- a/mobile/test/unit/presentation/actions/asset_debug_action_test.dart +++ b/mobile/test/unit/presentation/actions/asset_debug_action_test.dart @@ -16,8 +16,8 @@ void main() { await StoreService.I.put(StoreKey.advancedTroubleshooting, true); }); - tearDown(() { - context.dispose(); + tearDown(() async { + await context.dispose(); }); group('AssetDebugAction', () { diff --git a/mobile/test/unit/presentation/actions/cast_action_test.dart b/mobile/test/unit/presentation/actions/cast_action_test.dart index 45f1a842f5..7c16ea398e 100644 --- a/mobile/test/unit/presentation/actions/cast_action_test.dart +++ b/mobile/test/unit/presentation/actions/cast_action_test.dart @@ -13,8 +13,8 @@ void main() { context = await PresentationContext.create(); }); - tearDown(() { - context.dispose(); + tearDown(() async { + await context.dispose(); }); void Function(bool) captureConnectionListener() => diff --git a/mobile/test/unit/presentation/actions/delete_action_test.dart b/mobile/test/unit/presentation/actions/delete_action_test.dart index 4b75b01eaf..29d81a5c2c 100644 --- a/mobile/test/unit/presentation/actions/delete_action_test.dart +++ b/mobile/test/unit/presentation/actions/delete_action_test.dart @@ -32,7 +32,7 @@ void main() { tearDown(() async { debugDefaultTargetPlatformOverride = null; await StoreService.I.put(StoreKey.manageLocalMediaAndroid, false); - context.dispose(); + await context.dispose(); }); RemoteAsset owned({AssetVisibility visibility = .timeline, DateTime? deletedAt, String? localId}) => diff --git a/mobile/test/unit/presentation/actions/download_tag_action_test.dart b/mobile/test/unit/presentation/actions/download_tag_action_test.dart index 382b12ec83..de003ffed5 100644 --- a/mobile/test/unit/presentation/actions/download_tag_action_test.dart +++ b/mobile/test/unit/presentation/actions/download_tag_action_test.dart @@ -29,8 +29,8 @@ void main() { tagService = context.service.tag.service; }); - tearDown(() { - context.dispose(); + tearDown(() async { + await context.dispose(); }); RemoteAsset owned() => RemoteAssetFactory.create(ownerId: context.currentUser.id); diff --git a/mobile/test/unit/presentation/actions/edit_action_test.dart b/mobile/test/unit/presentation/actions/edit_action_test.dart index 063301f9c4..9e8dcfb4b2 100644 --- a/mobile/test/unit/presentation/actions/edit_action_test.dart +++ b/mobile/test/unit/presentation/actions/edit_action_test.dart @@ -28,8 +28,8 @@ void main() { assetService = context.service.asset.service; }); - tearDown(() { - context.dispose(); + tearDown(() async { + await context.dispose(); }); RemoteAsset owned({AssetType type = .image}) => diff --git a/mobile/test/unit/presentation/actions/favorite_action_test.dart b/mobile/test/unit/presentation/actions/favorite_action_test.dart index cb4d6130ab..4d61965b1f 100644 --- a/mobile/test/unit/presentation/actions/favorite_action_test.dart +++ b/mobile/test/unit/presentation/actions/favorite_action_test.dart @@ -20,8 +20,8 @@ void main() { assetService = context.service.asset.service; }); - tearDown(() { - context.dispose(); + tearDown(() async { + await context.dispose(); }); RemoteAsset owned({bool isFavorite = false}) => diff --git a/mobile/test/unit/presentation/actions/lock_action_test.dart b/mobile/test/unit/presentation/actions/lock_action_test.dart index f5ffbd73d4..9bad0f5a43 100644 --- a/mobile/test/unit/presentation/actions/lock_action_test.dart +++ b/mobile/test/unit/presentation/actions/lock_action_test.dart @@ -18,8 +18,8 @@ void main() { assetService = context.service.asset.service; }); - tearDown(() { - context.dispose(); + tearDown(() async { + await context.dispose(); }); RemoteAsset owned({AssetVisibility visibility = .timeline}) => diff --git a/mobile/test/unit/presentation/actions/open_in_browser_action_test.dart b/mobile/test/unit/presentation/actions/open_in_browser_action_test.dart index 0ed99e7704..edd9ec4a7c 100644 --- a/mobile/test/unit/presentation/actions/open_in_browser_action_test.dart +++ b/mobile/test/unit/presentation/actions/open_in_browser_action_test.dart @@ -13,8 +13,8 @@ void main() { context = await PresentationContext.create(); }); - tearDown(() { - context.dispose(); + tearDown(() async { + await context.dispose(); }); group('webPathFor', () { diff --git a/mobile/test/unit/presentation/actions/partner_action_test.dart b/mobile/test/unit/presentation/actions/partner_action_test.dart index a1d645ffec..786d2d7f60 100644 --- a/mobile/test/unit/presentation/actions/partner_action_test.dart +++ b/mobile/test/unit/presentation/actions/partner_action_test.dart @@ -19,8 +19,8 @@ void main() { partnerService = context.service.partner.service; }); - tearDown(() { - context.dispose(); + tearDown(() async { + await context.dispose(); }); List overrides({List candidates = const []}) => [ diff --git a/mobile/test/unit/presentation/actions/restore_action_test.dart b/mobile/test/unit/presentation/actions/restore_action_test.dart index 4597e7ccb0..530e1b76e9 100644 --- a/mobile/test/unit/presentation/actions/restore_action_test.dart +++ b/mobile/test/unit/presentation/actions/restore_action_test.dart @@ -18,8 +18,8 @@ void main() { assetService = context.service.asset.service; }); - tearDown(() { - context.dispose(); + tearDown(() async { + await context.dispose(); }); RemoteAsset owned({bool trashed = true}) => diff --git a/mobile/test/unit/presentation/actions/share_action_test.dart b/mobile/test/unit/presentation/actions/share_action_test.dart index 9faa6e3a5f..20cc25b73b 100644 --- a/mobile/test/unit/presentation/actions/share_action_test.dart +++ b/mobile/test/unit/presentation/actions/share_action_test.dart @@ -1,12 +1,8 @@ -import 'package:drift/drift.dart'; -import 'package:drift/native.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.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/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/presentation/actions/share.action.dart'; @@ -21,18 +17,12 @@ import '../presentation_context.dart'; void main() { late PresentationContext context; - setUpAll(() async { - final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - await SettingsRepository.ensureInitialized(db); - }); - setUp(() async { context = await PresentationContext.create(); - await SettingsRepository.instance.clear([SettingsKey.shareFileType]); }); - tearDown(() { - context.dispose(); + tearDown(() async { + await context.dispose(); }); late BuildContext actionContext; diff --git a/mobile/test/unit/presentation/actions/stack_action_test.dart b/mobile/test/unit/presentation/actions/stack_action_test.dart index fdaf83886c..11321c28af 100644 --- a/mobile/test/unit/presentation/actions/stack_action_test.dart +++ b/mobile/test/unit/presentation/actions/stack_action_test.dart @@ -18,8 +18,8 @@ void main() { assetService = context.service.asset.service; }); - tearDown(() { - context.dispose(); + tearDown(() async { + await context.dispose(); }); RemoteAsset owned({String? stackId}) => RemoteAssetFactory.create(ownerId: context.currentUser.id, stackId: stackId); diff --git a/mobile/test/unit/presentation/actions/upload_action_test.dart b/mobile/test/unit/presentation/actions/upload_action_test.dart index 2489f3100c..8aeb7a915f 100644 --- a/mobile/test/unit/presentation/actions/upload_action_test.dart +++ b/mobile/test/unit/presentation/actions/upload_action_test.dart @@ -27,8 +27,8 @@ void main() { uploadService = context.service.upload; }); - tearDown(() { - context.dispose(); + tearDown(() async { + await context.dispose(); }); List uploadOverrides() => [ diff --git a/mobile/test/unit/presentation/partner_page_test.dart b/mobile/test/unit/presentation/partner_page_test.dart index e6ff8fc83f..2cabbb9d4c 100644 --- a/mobile/test/unit/presentation/partner_page_test.dart +++ b/mobile/test/unit/presentation/partner_page_test.dart @@ -15,7 +15,7 @@ void main() { late PresentationContext context; setUp(() async => context = await PresentationContext.create()); - tearDown(() => context.dispose()); + tearDown(() async => await context.dispose()); group('PartnerSharedByList', () { testWidgets('shows the empty-state add button when there are no partners', (tester) async { diff --git a/mobile/test/unit/presentation/presentation_context.dart b/mobile/test/unit/presentation/presentation_context.dart index 43c76c8bf8..5b23890ef8 100644 --- a/mobile/test/unit/presentation/presentation_context.dart +++ b/mobile/test/unit/presentation/presentation_context.dart @@ -11,6 +11,7 @@ import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/presentation/actions/action.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; @@ -37,7 +38,6 @@ class PresentationContext { service = ServiceMocks(), repository = RepositoryMocks() { setup(); - addTearDown(dispose); } static const String serverEndpoint = 'http://localhost:3000'; @@ -75,6 +75,7 @@ class PresentationContext { await StoreService.I.put(StoreKey.serverEndpoint, serverEndpoint); _db = db; } + await SettingsRepository.ensureInitialized(_db!); return PresentationContext._(user: UserFactory.createDto()); } @@ -82,7 +83,8 @@ class PresentationContext { when(service.user.tryGetMyUser).thenReturn(currentUser); } - void dispose() { + Future dispose() async { + await SettingsRepository.reset(); service.resetAll(); } } From f1a90b7f36ad9f28c9c31f9d19fbd490a94f4cb4 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Mon, 3 Aug 2026 11:06:38 +0200 Subject: [PATCH 44/81] feat: log hint about downgrades when migration is missing (#30493) --- server/src/repositories/database.repository.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index 3fc9fea2e3..1b5592d356 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -9,6 +9,7 @@ import semver from 'semver'; import { EXTENSION_NAMES, POSTGRES_VERSION_RANGE, + serverVersion, VECTOR_EXTENSIONS, VECTOR_INDEX_TABLES, VECTOR_VERSION_RANGE, @@ -382,6 +383,17 @@ export class DatabaseRepository { if (error) { this.logger.error(`Migrations failed: ${error}`); + + const missing = + error instanceof Error ? error.message.match(/previously executed migration (.+) is missing/u) : null; + if (missing) { + throw new Error( + `Migration "${missing[1]}" was already applied to this database but is not in this version of Immich (${serverVersion}). ` + + `This usually means the database was migrated by a newer version. Downgrades are not supported.`, + { cause: error }, + ); + } + throw error; } From b3718fd18a37d4cb82f2f20a1484c62ae0c80335 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 11:53:40 +0200 Subject: [PATCH 45/81] chore(deps): update dependency @testing-library/jest-dom to v7 (#30308) --- pnpm-lock.yaml | 16 ++++++++++------ web/package.json | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b5503526c1..44d920aaa9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -952,8 +952,8 @@ importers: specifier: ^4.2.4 version: 4.3.3(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0)) '@testing-library/jest-dom': - specifier: ^6.4.2 - version: 6.9.1 + specifier: ^7.0.0 + version: 7.0.0(@testing-library/dom@10.4.1) '@testing-library/svelte': specifier: ^5.2.8 version: 5.4.2(svelte@5.56.8(@typescript-eslint/types@8.65.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.49.0)(tsx@4.23.1)(yaml@2.9.0))(vitest@4.1.10) @@ -4995,9 +4995,11 @@ packages: resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} - '@testing-library/jest-dom@6.9.1': - resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} - engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + '@testing-library/jest-dom@7.0.0': + resolution: {integrity: sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==} + engines: {node: '>=22', npm: '>=6', yarn: '>=1'} + peerDependencies: + '@testing-library/dom': '>=10 <11' '@testing-library/svelte-core@1.1.3': resolution: {integrity: sha512-KkMAvXeWorxN2Yn0kdC1lfoAItxpoj4uOWzxK5leDrNxonLvS5nwBFvztrroyTszQ0Wf/EU6iLT8JhY5qcn22g==} @@ -6805,6 +6807,7 @@ packages: cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} + deprecated: v4 is no longer maintained, upgrade to v5 cron@4.4.0: resolution: {integrity: sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==} @@ -17751,9 +17754,10 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.9.1': + '@testing-library/jest-dom@7.0.0(@testing-library/dom@10.4.1)': dependencies: '@adobe/css-tools': 4.5.0 + '@testing-library/dom': 10.4.1 aria-query: 5.3.1 css.escape: 1.5.1 dom-accessibility-api: 0.6.3 diff --git a/web/package.json b/web/package.json index aeaeb3ee20..338e2f5abe 100644 --- a/web/package.json +++ b/web/package.json @@ -80,7 +80,7 @@ "@sveltejs/kit": "^2.56.1", "@sveltejs/vite-plugin-svelte": "7.2.0", "@tailwindcss/vite": "^4.2.4", - "@testing-library/jest-dom": "^6.4.2", + "@testing-library/jest-dom": "^7.0.0", "@testing-library/svelte": "^5.2.8", "@testing-library/user-event": "^14.5.2", "@trivago/prettier-plugin-sort-imports": "^6.0.2", From fb4a08c17ae7480429339c3631913881fc9a264a Mon Sep 17 00:00:00 2001 From: Ben Beckford Date: Mon, 3 Aug 2026 02:55:40 -0700 Subject: [PATCH 46/81] chore(server): test assetFileFilter (#30430) --- .../workflow/workflow-core-plugin.spec.ts | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts b/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts index 1ad40ecca7..8bd65af73f 100644 --- a/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts +++ b/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts @@ -406,6 +406,101 @@ describe('core plugin', () => { }); }); + describe('assetFileFilter', () => { + it('should match assets case-insensitively', async () => { + const { user } = await ctx.newUser(); + const [{ asset: asset1 }, { asset: asset2 }] = await Promise.all([ + ctx.newAsset({ ownerId: user.id, originalFileName: 'exampleFile.png' }), + ctx.newAsset({ ownerId: user.id, originalFileName: 'anotherfile.jpg' }), + ]); + + const workflow = await createWorkflow({ + ownerId: user.id, + trigger: WorkflowTrigger.AssetCreate, + steps: [ + { + method: 'immich-plugin-core#assetFileFilter', + config: { matchType: 'contains', pattern: 'File' }, + }, + { + method: 'immich-plugin-core#assetFavorite', + }, + ], + }); + + await expect( + ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset1.id }), + ).resolves.toBeUndefined(); + await expect( + ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset2.id }), + ).resolves.toBeUndefined(); + await expect(ctx.get(AssetRepository).getById(asset1.id)).resolves.toMatchObject({ isFavorite: true }); + await expect(ctx.get(AssetRepository).getById(asset2.id)).resolves.toMatchObject({ isFavorite: true }); + }); + + it('should match assets by regex', async () => { + const { user } = await ctx.newUser(); + const [{ asset: asset1 }, { asset: asset2 }] = await Promise.all([ + ctx.newAsset({ ownerId: user.id, originalFileName: 'exampleFile.png' }), + ctx.newAsset({ ownerId: user.id, originalFileName: 'anotherfile.jpg' }), + ]); + + const workflow = await createWorkflow({ + ownerId: user.id, + trigger: WorkflowTrigger.AssetCreate, + steps: [ + { + method: 'immich-plugin-core#assetFileFilter', + config: { matchType: 'regex', pattern: '.+png' }, + }, + { + method: 'immich-plugin-core#assetFavorite', + }, + ], + }); + + await expect( + ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset1.id }), + ).resolves.toBeUndefined(); + await expect( + ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset2.id }), + ).resolves.toBeUndefined(); + await expect(ctx.get(AssetRepository).getById(asset1.id)).resolves.toMatchObject({ isFavorite: true }); + await expect(ctx.get(AssetRepository).getById(asset2.id)).resolves.toMatchObject({ isFavorite: false }); + }); + + it('should filter assets by path if specified', async () => { + const { user } = await ctx.newUser(); + const [{ asset: asset1 }, { asset: asset2 }] = await Promise.all([ + ctx.newAsset({ ownerId: user.id, originalPath: '/library/folder/file1.png' }), + ctx.newAsset({ ownerId: user.id, originalPath: '/library/file2.png' }), + ]); + + const workflow = await createWorkflow({ + ownerId: user.id, + trigger: WorkflowTrigger.AssetCreate, + steps: [ + { + method: 'immich-plugin-core#assetFileFilter', + config: { matchType: 'contains', pattern: 'folder', usePath: true }, + }, + { + method: 'immich-plugin-core#assetFavorite', + }, + ], + }); + + await expect( + ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset1.id }), + ).resolves.toBeUndefined(); + await expect( + ctx.sut.handleAssetTrigger({ workflowId: workflow.id, assetId: asset2.id }), + ).resolves.toBeUndefined(); + await expect(ctx.get(AssetRepository).getById(asset1.id)).resolves.toMatchObject({ isFavorite: true }); + await expect(ctx.get(AssetRepository).getById(asset2.id)).resolves.toMatchObject({ isFavorite: false }); + }); + }); + describe('assetTypeFilter', () => { it('should favorite asset if it is a video', async () => { const { user } = await ctx.newUser(); From 4082fbf232f98285484b48459db1fb39bc559d32 Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 3 Aug 2026 20:28:18 +1000 Subject: [PATCH 47/81] fix(web): Fix face thumbnail when swapping merge direction (#30466) --- .../modals/PersonMergeSuggestionModal.svelte | 35 +++++++++++-------- .../[[assetId=id]]/FaceThumbnail.svelte | 5 ++- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/web/src/lib/modals/PersonMergeSuggestionModal.svelte b/web/src/lib/modals/PersonMergeSuggestionModal.svelte index 2f799c2939..94006ed5e7 100644 --- a/web/src/lib/modals/PersonMergeSuggestionModal.svelte +++ b/web/src/lib/modals/PersonMergeSuggestionModal.svelte @@ -63,13 +63,16 @@
{#if !choosePersonToMerge}
- + + {#key personToMerge.id} + + {/key}
@@ -101,14 +104,16 @@ } }} > - 0} - circle - shadow - url={getPeopleThumbnailUrl(personToBeMergedInto)} - altText={personToBeMergedInto.name} - widthStyle="100%" - /> + {#key personToBeMergedInto.id} + 0} + circle + shadow + url={getPeopleThumbnailUrl(personToBeMergedInto)} + altText={personToBeMergedInto.name} + widthStyle="100%" + /> + {/key} {:else}
diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/FaceThumbnail.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/FaceThumbnail.svelte index 4a594ddce1..8f2f82e9b5 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/FaceThumbnail.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/FaceThumbnail.svelte @@ -40,7 +40,10 @@ class:dark:border-immich-dark-primary={border} class:border-immich-primary={border} > - + + {#key person.id} + + {/key}
Date: Mon, 3 Aug 2026 10:42:44 +0000 Subject: [PATCH 48/81] feat(web): search album description in add-to-album modal (#30462) Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> --- .../components/asset-viewer/AlbumListItem.svelte | 3 +++ .../album-selection/album-selection-utils.spec.ts | 13 +++++++++++++ .../album-selection/album-selection-utils.ts | 6 +++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/web/src/lib/components/asset-viewer/AlbumListItem.svelte b/web/src/lib/components/asset-viewer/AlbumListItem.svelte index af40deb27a..3d4be59a2c 100644 --- a/web/src/lib/components/asset-viewer/AlbumListItem.svelte +++ b/web/src/lib/components/asset-viewer/AlbumListItem.svelte @@ -40,6 +40,9 @@ const albumNameArray: string[] = $derived.by(() => { let { albumName } = album; let findIndex = normalizeSearchString(albumName).indexOf(normalizeSearchString(searchQuery)); + if (findIndex === -1) { + return [albumName, '', '']; + } let findLength = searchQuery.length; return [ albumName.slice(0, findIndex), diff --git a/web/src/lib/components/shared-components/album-selection/album-selection-utils.spec.ts b/web/src/lib/components/shared-components/album-selection/album-selection-utils.spec.ts index 669ce22f85..c7c3413b84 100644 --- a/web/src/lib/components/shared-components/album-selection/album-selection-utils.spec.ts +++ b/web/src/lib/components/shared-components/album-selection/album-selection-utils.spec.ts @@ -108,6 +108,19 @@ describe('Album Modal', () => { ]); }); + it('search matches on description as well as name', () => { + const converter = new AlbumModalRowConverter(AlbumSortBy.MostRecentPhoto, SortOrder.Desc); + const holidayAlbum = albumFactory.build({ albumName: 'Vacances 2019', description: 'Crete' }); + const constructionAlbum = albumFactory.build({ albumName: 'Construction' }); + const modalRows = converter.toModalRows('Crete', [], [holidayAlbum, constructionAlbum], -1, []); + + expect(modalRows).toStrictEqual([ + createNewAlbumRow(false), + createSectionRow('ALBUMS'), + createAlbumRow(holidayAlbum, false), + ]); + }); + it('selection can select new album row', () => { const converter = new AlbumModalRowConverter(AlbumSortBy.MostRecentPhoto, SortOrder.Desc); const holidayAlbum = albumFactory.build({ albumName: 'Holidays' }); diff --git a/web/src/lib/components/shared-components/album-selection/album-selection-utils.ts b/web/src/lib/components/shared-components/album-selection/album-selection-utils.ts index ad96b27c06..5d4e46a063 100644 --- a/web/src/lib/components/shared-components/album-selection/album-selection-utils.ts +++ b/web/src/lib/components/shared-components/album-selection/album-selection-utils.ts @@ -46,10 +46,14 @@ export class AlbumModalRowConverter { const recentAlbumsToShow = search.length === 0 ? recentAlbums : []; const rows: AlbumModalRow[] = [{ type: AlbumModalRowType.NEW_ALBUM, selected: selectedRowIndex === 0 }]; + const normalizedSearch = normalizeSearchString(search); const filteredAlbums = sortAlbums( search.length > 0 && albums.length > 0 ? albums.filter((album) => { - return normalizeSearchString(album.albumName).includes(normalizeSearchString(search)); + return ( + normalizeSearchString(album.albumName).includes(normalizedSearch) || + normalizeSearchString(album.description).includes(normalizedSearch) + ); }) : albums, { sortBy: this.sortBy, orderBy: this.orderBy }, From 04453b72060bdcd0659d784a5c0c5eaf21be984b Mon Sep 17 00:00:00 2001 From: Gueye Papa Djadji Date: Mon, 3 Aug 2026 10:56:00 +0000 Subject: [PATCH 49/81] fix(server): reject invalid or deleted user when creating a partner (#30431) --- server/src/services/partner.service.spec.ts | 14 ++++++++++++++ server/src/services/partner.service.ts | 6 ++++++ 2 files changed, 20 insertions(+) diff --git a/server/src/services/partner.service.spec.ts b/server/src/services/partner.service.spec.ts index 029462a865..b947f3795a 100644 --- a/server/src/services/partner.service.spec.ts +++ b/server/src/services/partner.service.spec.ts @@ -54,6 +54,7 @@ describe(PartnerService.name, () => { const auth = AuthFactory.create({ id: user1.id }); mocks.partner.get.mockResolvedValue(void 0); + mocks.user.get.mockResolvedValue(user2); mocks.partner.create.mockResolvedValue(getForPartner(partner)); await expect(sut.create(auth, { sharedWithId: user2.id })).resolves.toBeDefined(); @@ -76,6 +77,19 @@ describe(PartnerService.name, () => { expect(mocks.partner.create).not.toHaveBeenCalled(); }); + + it('should throw an error when sharedWithId does not resolve to an existing (non-deleted) user', async () => { + const user1 = UserFactory.create(); + const user2 = UserFactory.create(); + const auth = AuthFactory.create({ id: user1.id }); + + mocks.partner.get.mockResolvedValue(void 0); + mocks.user.get.mockResolvedValue(void 0); + + await expect(sut.create(auth, { sharedWithId: user2.id })).rejects.toBeInstanceOf(BadRequestException); + + expect(mocks.partner.create).not.toHaveBeenCalled(); + }); }); describe('remove', () => { diff --git a/server/src/services/partner.service.ts b/server/src/services/partner.service.ts index cc950edb5b..26d7701077 100644 --- a/server/src/services/partner.service.ts +++ b/server/src/services/partner.service.ts @@ -16,6 +16,12 @@ export class PartnerService extends BaseService { throw new BadRequestException(`Partner already exists`); } + const user = await this.userRepository.get(sharedWithId, {}); + if (!user) { + this.logger.debug('Partner creation failed: user not found'); + throw new BadRequestException('Invalid user'); + } + const partner = await this.partnerRepository.create(partnerId); return this.mapPartner(partner, PartnerDirection.SharedBy); } From 089486535295309982988d8c52fe00678d17540d Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Mon, 3 Aug 2026 13:38:09 +0200 Subject: [PATCH 50/81] chore: typescript 7 extension for vscode (#30516) --- .vscode/extensions.json | 4 +++- .vscode/settings.json | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 399fedae33..782e0192f6 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -12,6 +12,8 @@ "editorconfig.editorconfig", "foxundermoon.shell-format", "timonwong.shellcheck", - "bluebrown.yamlfmt" + "bluebrown.yamlfmt", + // TODO TS 7 while it's still in preview + "typescriptteam.native-preview" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 30dac3216e..23d4385964 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -60,6 +60,8 @@ "**/dist/**": true, "**/node_modules/**": true }, + // TODO remove once the ts7 extension is stable + "js/ts.experimental.useTsgo": true, "js/ts.preferences.importModuleSpecifier": "non-relative", "search.exclude": { "**/.svelte-kit": true, From cee08a2320dbddb563a7fa8cca7979e4c954edb0 Mon Sep 17 00:00:00 2001 From: Santo Shakil Date: Mon, 3 Aug 2026 18:59:05 +0600 Subject: [PATCH 51/81] fix(server): remove the asset row when an upload fails after creating it (#30349) --- server/src/services/asset-media.service.spec.ts | 6 ++++++ server/src/services/asset-media.service.ts | 10 ++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index 5a9a85f179..14527debba 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -317,6 +317,12 @@ describe(AssetMediaService.name, () => { ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.asset.create).not.toHaveBeenCalled(); + expect(mocks.asset.remove).not.toHaveBeenCalled(); + expect(mocks.job.queue).toHaveBeenCalledWith({ + name: JobName.FileDelete, + data: { files: [file.originalPath, undefined] }, + }); + expect(mocks.event.emit).not.toHaveBeenCalled(); expect(mocks.user.updateUsage).not.toHaveBeenCalledWith(authStub.user1.user.id, file.size); expect(mocks.storage.utimes).not.toHaveBeenCalledWith( file.originalPath, diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index 818bd5eb91..1bb9d1e5b6 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -1,7 +1,7 @@ import { BadRequestException, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; import sanitize from 'sanitize-filename'; import { StorageCore } from 'src/cores/storage.core'; -import { AuthSharedLink } from 'src/database'; +import { Asset, AuthSharedLink } from 'src/database'; import { AssetBulkUploadCheckResponseDto, AssetMediaResponseDto, @@ -128,6 +128,7 @@ export class AssetMediaService extends BaseService { file: UploadFile, sidecarFile?: UploadFile, ): Promise { + let asset: Asset | undefined; try { await this.requireAccess({ auth, @@ -145,7 +146,7 @@ export class AssetMediaService extends BaseService { ); } - const asset = await this.assetRepository.create({ + asset = await this.assetRepository.create({ ownerId: auth.user.id, libraryId: null, @@ -215,6 +216,11 @@ export class AssetMediaService extends BaseService { return { status: AssetMediaStatus.DUPLICATE, id: duplicateId }; } + // clean up the asset row if one was created + if (asset) { + await this.assetRepository.remove({ id: asset.id }); + } + this.logger.error(`Error uploading file ${error}`, error?.stack); throw error; } From 774a9fd86845053c3bac682489517cd7e88c8d12 Mon Sep 17 00:00:00 2001 From: Azharul Haque <13651113+DrHaque@users.noreply.github.com> Date: Mon, 3 Aug 2026 09:30:15 -0400 Subject: [PATCH 52/81] fix(mobile): correct mislabeled Bengali locale entry (#30519) Fixes mislabeled locale entry: 'Bosnian (bl)': Locale('bn') actually loads Bengali, not Bosnian. --- mobile/lib/constants/locales.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/constants/locales.dart b/mobile/lib/constants/locales.dart index d689e6cc1e..429a3718da 100644 --- a/mobile/lib/constants/locales.dart +++ b/mobile/lib/constants/locales.dart @@ -7,7 +7,7 @@ const Map locales = { 'Arabic (ar)': Locale('ar'), 'Basque (eu)': Locale('eu'), 'Belarusian (be)': Locale('be'), - 'Bosnian (bl)': Locale('bn'), + 'Bengali (bn)': Locale('bn'), 'Brazilian Portuguese (pt_BR)': Locale('pt', 'BR'), 'Bulgarian (bg)': Locale('bg'), 'Catalan (ca)': Locale('ca'), From c2db36934f0586af32df0273b36fcb37078cd075 Mon Sep 17 00:00:00 2001 From: Adrien Fabre Date: Mon, 3 Aug 2026 16:00:04 +0200 Subject: [PATCH 53/81] feat: Display the number of selected items in AlbumPickerModal title (#30485) --- i18n/en.json | 1 + .../lib/components/SchemaAlbumPicker.svelte | 2 +- web/src/lib/modals/AlbumPickerModal.spec.ts | 58 +++++++++++++++++++ web/src/lib/modals/AlbumPickerModal.svelte | 11 +++- .../lib/modals/AssetAddToAlbumModal.svelte | 2 +- 5 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 web/src/lib/modals/AlbumPickerModal.spec.ts diff --git a/i18n/en.json b/i18n/en.json index 1dbd50e294..857265f004 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -30,6 +30,7 @@ "add_to_album_bottom_sheet_added": "Added to {album}", "add_to_album_bottom_sheet_already_exists": "Already in {album}", "add_to_album_bottom_sheet_some_local_assets": "Some local assets could not be added to album", + "add_to_album_item_count": "Add {count, plural, one {# item} other {# items}} to album", "add_to_albums": "Add to albums", "add_to_albums_count": "Add to albums ({count})", "add_to_bottom_bar": "Add to", diff --git a/web/src/lib/components/SchemaAlbumPicker.svelte b/web/src/lib/components/SchemaAlbumPicker.svelte index b7eb9dd7dd..e4eb271a3d 100644 --- a/web/src/lib/components/SchemaAlbumPicker.svelte +++ b/web/src/lib/components/SchemaAlbumPicker.svelte @@ -14,7 +14,7 @@ let { array, label, description, albumIds = $bindable([]) }: Props = $props(); const onAlbums = async () => { - const albums = await modalManager.show(AlbumPickerModal); + const albums = await modalManager.show(AlbumPickerModal, {}); if (!albums || albums.length === 0) { return; } diff --git a/web/src/lib/modals/AlbumPickerModal.spec.ts b/web/src/lib/modals/AlbumPickerModal.spec.ts new file mode 100644 index 0000000000..4245f3e17f --- /dev/null +++ b/web/src/lib/modals/AlbumPickerModal.spec.ts @@ -0,0 +1,58 @@ +import { render, screen, waitFor } from '@testing-library/svelte'; +import { init, register, waitLocale } from 'svelte-i18n'; +import { getAnimateMock } from '$lib/__mocks__/animate.mock'; +import { getIntersectionObserverMock } from '$lib/__mocks__/intersection-observer.mock'; +import { sdkMock } from '$lib/__mocks__/sdk.mock'; +import { getVisualViewportMock } from '$lib/__mocks__/visual-viewport.mock'; +import AlbumPickerModal from './AlbumPickerModal.svelte'; + +describe('AlbumPickerModal component', () => { + const onClose = vi.fn(); + + beforeAll(async () => { + await init({ fallbackLocale: 'en-US' }); + register('en-US', () => import('$i18n/en.json')); + await waitLocale('en-US'); + }); + + beforeEach(() => { + vi.stubGlobal('IntersectionObserver', getIntersectionObserverMock()); + vi.stubGlobal('visualViewport', getVisualViewportMock()); + vi.resetAllMocks(); + Element.prototype.animate = getAnimateMock(); + }); + + afterAll(async () => { + await waitFor(() => { + expect(document.body.style.pointerEvents).not.toBe('none'); + }); + }); + + it('shows the singular selection count title when selectedItemsCount is 1', async () => { + // Called by onMount() + sdkMock.getAllAlbums.mockResolvedValueOnce([]); + + render(AlbumPickerModal, { props: { onClose, selectedItemsCount: 1 } }); + + expect(await screen.findByText('Add 1 item to album')).toBeInTheDocument(); + expect(screen.queryByText('Select albums')).not.toBeInTheDocument(); + }); + + it('shows the plural selection count title when selectedItemsCount is greater than 1', async () => { + sdkMock.getAllAlbums.mockResolvedValueOnce([]); + + render(AlbumPickerModal, { props: { onClose, selectedItemsCount: 3 } }); + + expect(await screen.findByText('Add 3 items to album')).toBeInTheDocument(); + expect(screen.queryByText('Select albums')).not.toBeInTheDocument(); + }); + + it('shows the generic title when selectedItemsCount is not provided', async () => { + sdkMock.getAllAlbums.mockResolvedValueOnce([]); + + render(AlbumPickerModal, { props: { onClose } }); + + expect(await screen.findByText('Select albums')).toBeInTheDocument(); + expect(screen.queryByText('Add 1 item to album')).not.toBeInTheDocument(); + }); +}); diff --git a/web/src/lib/modals/AlbumPickerModal.svelte b/web/src/lib/modals/AlbumPickerModal.svelte index 561deeac23..19430e271c 100644 --- a/web/src/lib/modals/AlbumPickerModal.svelte +++ b/web/src/lib/modals/AlbumPickerModal.svelte @@ -23,9 +23,10 @@ type Props = { onClose: (albums?: AlbumResponseDto[]) => void; + selectedItemsCount?: number; }; - let { onClose }: Props = $props(); + let { onClose, selectedItemsCount }: Props = $props(); onMount(async () => { albums = await getAllAlbums({}); @@ -147,9 +148,15 @@ } } }; + + const title = $derived( + selectedItemsCount === undefined + ? $t('select_albums') + : $t('add_to_album_item_count', { values: { count: selectedItemsCount } }), + ); - +
{#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 54/81] 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 55/81] 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 56/81] 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 57/81] 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 58/81] 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 @@