From 11403abfbc5e6dfc366470a944e20b4def48a218 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Wed, 13 Nov 2024 19:49:25 -0500 Subject: [PATCH 01/15] feat(mobile): new video slider ui (#14126) --- mobile/lib/constants/immich_colors.dart | 2 +- .../asset_viewer/bottom_gallery_bar.dart | 75 +++++++----- .../asset_viewer/formatted_duration.dart | 5 +- .../widgets/asset_viewer/video_controls.dart | 27 +---- .../asset_viewer/video_mute_button.dart | 23 ---- .../widgets/asset_viewer/video_position.dart | 110 +++++++++++------- 6 files changed, 120 insertions(+), 122 deletions(-) delete mode 100644 mobile/lib/widgets/asset_viewer/video_mute_button.dart diff --git a/mobile/lib/constants/immich_colors.dart b/mobile/lib/constants/immich_colors.dart index 37e98a7f70..d63928b5b8 100644 --- a/mobile/lib/constants/immich_colors.dart +++ b/mobile/lib/constants/immich_colors.dart @@ -20,7 +20,7 @@ const String defaultColorPresetName = "indigo"; const Color immichBrandColorLight = Color(0xFF4150AF); const Color immichBrandColorDark = Color(0xFFACCBFA); const Color whiteOpacity75 = Color.fromARGB((0.75 * 255) ~/ 1, 255, 255, 255); -const Color blackOpacity40 = Color.fromARGB((0.40 * 255) ~/ 1, 0, 0, 0); +const Color blackOpacity90 = Color.fromARGB((0.90 * 255) ~/ 1, 0, 0, 0); final Map _themePresetsMap = { ImmichColorPreset.indigo: ImmichTheme( diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart index f550857b9d..eadaf0bf9f 100644 --- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart +++ b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart @@ -5,6 +5,7 @@ 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/immich_colors.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/current_album.provider.dart'; @@ -327,39 +328,51 @@ class BottomGalleryBar extends ConsumerWidget { child: AnimatedOpacity( duration: const Duration(milliseconds: 100), opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0, - child: Column( - children: [ - Visibility( - visible: showVideoPlayerControls, - child: const VideoControls(), + child: DecoratedBox( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [blackOpacity90, Colors.transparent], ), - BottomNavigationBar( - backgroundColor: Colors.black.withOpacity(0.4), - unselectedIconTheme: const IconThemeData(color: Colors.white), - selectedIconTheme: const IconThemeData(color: Colors.white), - unselectedLabelStyle: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.w500, - height: 2.3, - ), - selectedLabelStyle: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.w500, - height: 2.3, - ), - unselectedFontSize: 14, - selectedFontSize: 14, - selectedItemColor: Colors.white, - unselectedItemColor: Colors.white, - showSelectedLabels: true, - showUnselectedLabels: true, - items: - albumActions.map((e) => e.keys.first).toList(growable: false), - onTap: (index) { - albumActions[index].values.first.call(index); - }, + ), + position: DecorationPosition.background, + child: Padding( + padding: EdgeInsets.only(top: 40.0), + child: Column( + children: [ + if (showVideoPlayerControls) const VideoControls(), + BottomNavigationBar( + elevation: 0.0, + backgroundColor: Colors.transparent, + unselectedIconTheme: const IconThemeData(color: Colors.white), + selectedIconTheme: const IconThemeData(color: Colors.white), + unselectedLabelStyle: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + height: 2.3, + ), + selectedLabelStyle: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + height: 2.3, + ), + unselectedFontSize: 14, + selectedFontSize: 14, + selectedItemColor: Colors.white, + unselectedItemColor: Colors.white, + showSelectedLabels: true, + showUnselectedLabels: true, + items: albumActions + .map((e) => e.keys.first) + .toList(growable: false), + onTap: (index) { + albumActions[index].values.first.call(index); + }, + ), + ], ), - ], + ), ), ), ); diff --git a/mobile/lib/widgets/asset_viewer/formatted_duration.dart b/mobile/lib/widgets/asset_viewer/formatted_duration.dart index 4a334cd7cc..a34aab7d12 100644 --- a/mobile/lib/widgets/asset_viewer/formatted_duration.dart +++ b/mobile/lib/widgets/asset_viewer/formatted_duration.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:immich_mobile/constants/immich_colors.dart'; @pragma('vm:prefer-inline') String _formatDuration(Duration position) { @@ -24,8 +23,8 @@ class FormattedDuration extends StatelessWidget { _formatDuration(data), style: const TextStyle( fontSize: 14.0, - color: whiteOpacity75, - fontWeight: FontWeight.normal, + color: Colors.white, + fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, ), diff --git a/mobile/lib/widgets/asset_viewer/video_controls.dart b/mobile/lib/widgets/asset_viewer/video_controls.dart index c96d58d374..e4d78324c8 100644 --- a/mobile/lib/widgets/asset_viewer/video_controls.dart +++ b/mobile/lib/widgets/asset_viewer/video_controls.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/immich_colors.dart'; -import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; import 'package:immich_mobile/widgets/asset_viewer/video_position.dart'; /// The video controls for the [videoPlayerControlsProvider] @@ -12,24 +10,11 @@ class VideoControls extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final isPortrait = MediaQuery.orientationOf(context) == Orientation.portrait; - return AnimatedOpacity( - opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0, - duration: const Duration(milliseconds: 100), - child: isPortrait - ? const ColoredBox( - color: blackOpacity40, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 24.0), - child: VideoPosition(), - ), - ) - : const ColoredBox( - color: blackOpacity40, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 128.0), - child: VideoPosition(), - ), - ), - ); + return isPortrait + ? const VideoPosition() + : const Padding( + padding: EdgeInsets.symmetric(horizontal: 60.0), + child: VideoPosition(), + ); } } diff --git a/mobile/lib/widgets/asset_viewer/video_mute_button.dart b/mobile/lib/widgets/asset_viewer/video_mute_button.dart deleted file mode 100644 index da0f6f3174..0000000000 --- a/mobile/lib/widgets/asset_viewer/video_mute_button.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; - -class VideoMuteButton extends ConsumerWidget { - const VideoMuteButton({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return IconButton( - icon: ref.watch( - videoPlayerControlsProvider.select((value) => value.mute), - ) - ? const Icon(Icons.volume_off) - : const Icon(Icons.volume_up), - onPressed: () => - ref.read(videoPlayerControlsProvider.notifier).toggleMute(), - color: Colors.white, - padding: const EdgeInsets.all(0), - alignment: Alignment.centerRight, - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/video_position.dart b/mobile/lib/widgets/asset_viewer/video_position.dart index 0512785782..ef309b9c85 100644 --- a/mobile/lib/widgets/asset_viewer/video_position.dart +++ b/mobile/lib/widgets/asset_viewer/video_position.dart @@ -7,7 +7,6 @@ import 'package:immich_mobile/constants/immich_colors.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; import 'package:immich_mobile/widgets/asset_viewer/formatted_duration.dart'; -import 'package:immich_mobile/widgets/asset_viewer/video_mute_button.dart'; class VideoPosition extends HookConsumerWidget { const VideoPosition({super.key}); @@ -20,38 +19,52 @@ class VideoPosition extends HookConsumerWidget { final wasPlaying = useRef(true); return duration == Duration.zero ? const _VideoPositionPlaceholder() - : Row( + : Column( children: [ - FormattedDuration(position), - Expanded( - child: Slider( - value: min( - position.inMicroseconds / duration.inMicroseconds * 100, - 100, - ), - min: 0, - max: 100, - thumbColor: Colors.white, - activeColor: Colors.white, - inactiveColor: whiteOpacity75, - onChangeStart: (value) { - final state = ref.read(videoPlaybackValueProvider).state; - wasPlaying.value = state != VideoPlaybackState.paused; - ref.read(videoPlayerControlsProvider.notifier).pause(); - }, - onChangeEnd: (value) { - if (wasPlaying.value) { - ref.read(videoPlayerControlsProvider.notifier).play(); - } - }, - onChanged: (position) { - ref.read(videoPlayerControlsProvider.notifier).position = - position; - }, + Padding( + // align with slider's inherent padding + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + FormattedDuration(position), + FormattedDuration(duration), + ], ), ), - FormattedDuration(duration), - const VideoMuteButton(), + Row( + children: [ + Expanded( + child: Slider( + value: min( + position.inMicroseconds / duration.inMicroseconds * 100, + 100, + ), + min: 0, + max: 100, + thumbColor: Colors.white, + activeColor: Colors.white, + inactiveColor: whiteOpacity75, + onChangeStart: (value) { + final state = + ref.read(videoPlaybackValueProvider).state; + wasPlaying.value = state != VideoPlaybackState.paused; + ref.read(videoPlayerControlsProvider.notifier).pause(); + }, + onChangeEnd: (value) { + if (wasPlaying.value) { + ref.read(videoPlayerControlsProvider.notifier).play(); + } + }, + onChanged: (position) { + ref + .read(videoPlayerControlsProvider.notifier) + .position = position; + }, + ), + ), + ], + ), ], ); } @@ -64,22 +77,33 @@ class _VideoPositionPlaceholder extends StatelessWidget { @override Widget build(BuildContext context) { - return const Row( + return const Column( children: [ - FormattedDuration(Duration.zero), - Expanded( - child: Slider( - value: 0.0, - min: 0, - max: 100, - thumbColor: Colors.white, - activeColor: Colors.white, - inactiveColor: whiteOpacity75, - onChanged: _onChangedDummy, + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + FormattedDuration(Duration.zero), + FormattedDuration(Duration.zero), + ], ), ), - FormattedDuration(Duration.zero), - VideoMuteButton(), + Row( + children: [ + Expanded( + child: Slider( + value: 0.0, + min: 0, + max: 100, + thumbColor: Colors.white, + activeColor: Colors.white, + inactiveColor: whiteOpacity75, + onChanged: _onChangedDummy, + ), + ), + ], + ), ], ); } From 9203a61709b561c27475066a5d736ad9fe302a55 Mon Sep 17 00:00:00 2001 From: Lukas Date: Thu, 14 Nov 2024 02:07:04 -0500 Subject: [PATCH 02/15] fix(server): Some MTS videos fail to generate thumbnail (#14134) * Stop skipping of all frames in MTS video * Only skip flag for mts videos * Fix lint checks * Adds test * Add comment for why flag is removed --- server/src/interfaces/media.interface.ts | 7 ++++++- server/src/services/media.service.spec.ts | 16 ++++++++++++++++ server/src/services/media.service.ts | 13 +++++++++---- server/src/utils/media.ts | 19 ++++++++++++++----- server/test/fixtures/media.stub.ts | 7 +++++++ 5 files changed, 52 insertions(+), 10 deletions(-) diff --git a/server/src/interfaces/media.interface.ts b/server/src/interfaces/media.interface.ts index fe11d17b5f..468a6ad88d 100644 --- a/server/src/interfaces/media.interface.ts +++ b/server/src/interfaces/media.interface.ts @@ -114,7 +114,12 @@ export interface ImageBuffer { } export interface VideoCodecSWConfig { - getCommand(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream: AudioStreamInfo): TranscodeCommand; + getCommand( + target: TranscodeTarget, + videoStream: VideoStreamInfo, + audioStream: AudioStreamInfo, + format?: VideoFormat, + ): TranscodeCommand; } export interface VideoCodecHWConfig extends VideoCodecSWConfig { diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index df1a04dff8..069376b8d3 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -487,6 +487,22 @@ describe(MediaService.name, () => { }), ); }); + it('should not skip intra frames for MTS file', async () => { + mediaMock.probe.mockResolvedValue(probeStub.videoStreamMTS); + assetMock.getById.mockResolvedValue(assetStub.video); + await sut.handleGenerateThumbnails({ id: assetStub.video.id }); + + expect(mediaMock.transcode).toHaveBeenCalledWith( + '/original/path.ext', + 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + expect.objectContaining({ + inputOptions: ['-sws_flags accurate_rnd+full_chroma_int'], + outputOptions: expect.any(Array), + progress: expect.any(Object), + twoPass: false, + }), + ); + }); it('should use scaling divisible by 2 even when using quick sync', async () => { mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p); diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 0aa6bd03fb..770e26b243 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -239,7 +239,7 @@ export class MediaService extends BaseService { const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, image.thumbnail.format); this.storageCore.ensureFolders(previewPath); - const { audioStreams, videoStreams } = await this.mediaRepository.probe(asset.originalPath); + const { format, audioStreams, videoStreams } = await this.mediaRepository.probe(asset.originalPath); const mainVideoStream = this.getMainStream(videoStreams); if (!mainVideoStream) { throw new Error(`No video streams found for asset ${asset.id}`); @@ -248,9 +248,14 @@ export class MediaService extends BaseService { const previewConfig = ThumbnailConfig.create({ ...ffmpeg, targetResolution: image.preview.size.toString() }); const thumbnailConfig = ThumbnailConfig.create({ ...ffmpeg, targetResolution: image.thumbnail.size.toString() }); - - const previewOptions = previewConfig.getCommand(TranscodeTarget.VIDEO, mainVideoStream, mainAudioStream); - const thumbnailOptions = thumbnailConfig.getCommand(TranscodeTarget.VIDEO, mainVideoStream, mainAudioStream); + const previewOptions = previewConfig.getCommand(TranscodeTarget.VIDEO, mainVideoStream, mainAudioStream, format); + const thumbnailOptions = thumbnailConfig.getCommand( + TranscodeTarget.VIDEO, + mainVideoStream, + mainAudioStream, + format, + ); + this.logger.error(format.formatName); await this.mediaRepository.transcode(asset.originalPath, previewPath, previewOptions); await this.mediaRepository.transcode(asset.originalPath, thumbnailPath, thumbnailOptions); diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts index f61b472b75..98d3c7fdbb 100644 --- a/server/src/utils/media.ts +++ b/server/src/utils/media.ts @@ -6,6 +6,7 @@ import { TranscodeCommand, VideoCodecHWConfig, VideoCodecSWConfig, + VideoFormat, VideoStreamInfo, } from 'src/interfaces/media.interface'; @@ -77,9 +78,14 @@ export class BaseConfig implements VideoCodecSWConfig { return handler; } - getCommand(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) { + getCommand( + target: TranscodeTarget, + videoStream: VideoStreamInfo, + audioStream?: AudioStreamInfo, + format?: VideoFormat, + ) { const options = { - inputOptions: this.getBaseInputOptions(videoStream), + inputOptions: this.getBaseInputOptions(videoStream, format), outputOptions: [...this.getBaseOutputOptions(target, videoStream, audioStream), '-v verbose'], twoPass: this.eligibleForTwoPass(), progress: { frameCount: videoStream.frameCount, percentInterval: 5 }, @@ -101,7 +107,7 @@ export class BaseConfig implements VideoCodecSWConfig { } // eslint-disable-next-line @typescript-eslint/no-unused-vars - getBaseInputOptions(videoStream: VideoStreamInfo): string[] { + getBaseInputOptions(videoStream: VideoStreamInfo, format?: VideoFormat): string[] { return this.getInputThreadOptions(); } @@ -377,8 +383,11 @@ export class ThumbnailConfig extends BaseConfig { return new ThumbnailConfig(config); } - getBaseInputOptions(): string[] { - return ['-skip_frame nointra', '-sws_flags accurate_rnd+full_chroma_int']; + getBaseInputOptions(videoStream: VideoStreamInfo, format?: VideoFormat): string[] { + // skip_frame nointra skips all frames for some MPEG-TS files. Look at ffmpeg tickets 7950 and 7895 for more details. + return format?.formatName === 'mpegts' + ? ['-sws_flags accurate_rnd+full_chroma_int'] + : ['-skip_frame nointra', '-sws_flags accurate_rnd+full_chroma_int']; } getBaseOutputOptions() { diff --git a/server/test/fixtures/media.stub.ts b/server/test/fixtures/media.stub.ts index 082959c227..de11c23f0a 100644 --- a/server/test/fixtures/media.stub.ts +++ b/server/test/fixtures/media.stub.ts @@ -95,6 +95,13 @@ export const probeStub = { ...probeStubDefault, videoStreams: [{ ...probeStubDefaultVideoStream[0], bitrate: 40_000_000 }], }), + videoStreamMTS: Object.freeze({ + ...probeStubDefault, + format: { + ...probeStubDefaultFormat, + formatName: 'mpegts', + }, + }), videoStreamHDR: Object.freeze({ ...probeStubDefault, videoStreams: [ From 0b3742cf13071ffa74d4923df0a0a047fc20a3d7 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 14 Nov 2024 08:43:25 -0600 Subject: [PATCH 03/15] chore(web): migration svelte 5 syntax (#13883) --- web/package-lock.json | 6 +- web/package.json | 6 +- .../actions/__test__/focus-trap-test.svelte | 10 +- web/src/lib/actions/autogrow.ts | 2 +- .../lib/actions/context-menu-navigation.ts | 8 +- web/src/lib/actions/list-navigation.ts | 9 +- .../admin-page/delete-confirm-dialogue.svelte | 35 ++- .../admin-page/jobs/job-tile-button.svelte | 18 +- .../admin-page/jobs/job-tile-status.svelte | 13 +- .../admin-page/jobs/job-tile.svelte | 64 ++-- .../admin-page/jobs/jobs-panel.svelte | 12 +- .../jobs/storage-migration-description.svelte | 15 +- .../admin-page/restore-dialogue.svelte | 22 +- .../server-stats/server-stats-panel.svelte | 22 +- .../admin-page/server-stats/stats-card.svelte | 16 +- .../admin-page/settings/admin-settings.svelte | 24 +- .../settings/auth/auth-settings.svelte | 79 ++--- .../backup-settings/backup-settings.svelte | 57 ++-- .../settings/ffmpeg/ffmpeg-settings.svelte | 71 +++-- .../settings/image/image-settings.svelte | 41 ++- .../settings/job-settings/job-settings.svelte | 31 +- .../library-settings/library-settings.svelte | 65 ++-- .../logging-settings/logging-settings.svelte | 22 +- .../machine-learning-settings.svelte | 51 ++-- .../settings/map-settings/map-settings.svelte | 55 ++-- .../metadata-settings.svelte | 22 +- .../new-version-check-settings.svelte | 22 +- .../notification-settings.svelte | 41 +-- .../settings/server/server-settings.svelte | 31 +- .../storage-template-settings.svelte | 170 ++++++----- .../supported-datetime-panel.svelte | 6 +- .../settings/theme/theme-settings.svelte | 24 +- .../trash-settings/trash-settings.svelte | 29 +- .../user-settings/user-settings.svelte | 25 +- .../album-page/__tests__/album-card.spec.ts | 15 +- .../album-page/album-card-group.svelte | 37 ++- .../components/album-page/album-card.svelte | 25 +- .../components/album-page/album-cover.svelte | 17 +- .../album-page/album-description.svelte | 10 +- .../album-page/album-options.svelte | 38 ++- .../album-page/album-summary.svelte | 9 +- .../components/album-page/album-title.svelte | 20 +- .../components/album-page/album-viewer.svelte | 22 +- .../album-page/albums-controls.svelte | 94 +++--- .../components/album-page/albums-list.svelte | 78 +++-- .../album-page/albums-table-header.svelte | 28 +- .../album-page/albums-table-row.svelte | 18 +- .../components/album-page/albums-table.svelte | 13 +- .../album-page/share-info-modal.svelte | 20 +- .../album-page/user-selection-modal.svelte | 28 +- .../actions/add-to-album-action.svelte | 12 +- .../actions/archive-action.svelte | 8 +- .../asset-viewer/actions/close-action.svelte | 8 +- .../asset-viewer/actions/delete-action.svelte | 12 +- .../actions/download-action.svelte | 10 +- .../actions/favorite-action.svelte | 13 +- .../actions/motion-photo-action.svelte | 10 +- .../actions/next-asset-action.svelte | 6 +- .../actions/previous-asset-action.svelte | 6 +- .../actions/restore-action.svelte | 8 +- .../actions/set-album-cover-action.svelte | 8 +- .../actions/set-profile-picture-action.svelte | 8 +- .../asset-viewer/actions/share-action.svelte | 15 +- .../actions/show-detail-action.svelte | 8 +- .../actions/unstack-action.svelte | 8 +- .../asset-viewer/activity-status.svelte | 18 +- .../asset-viewer/activity-viewer.svelte | 99 ++++--- .../album-list-item-details.svelte | 6 +- .../asset-viewer/album-list-item.svelte | 18 +- .../asset-viewer/asset-viewer-nav-bar.svelte | 61 ++-- .../asset-viewer/asset-viewer.svelte | 152 +++++----- .../detail-panel-description.svelte | 14 +- .../asset-viewer/detail-panel-location.svelte | 14 +- .../detail-panel-star-rating.svelte | 10 +- .../asset-viewer/detail-panel-tags.svelte | 16 +- .../asset-viewer/detail-panel.svelte | 88 +++--- .../asset-viewer/download-panel.svelte | 2 +- .../editor/crop-tool/crop-area.svelte | 26 +- .../editor/crop-tool/crop-preset.svelte | 36 ++- .../editor/crop-tool/crop-tool.svelte | 17 +- .../asset-viewer/editor/editor-panel.svelte | 22 +- .../asset-viewer/navigation-area.svelte | 15 +- .../asset-viewer/panorama-viewer.svelte | 15 +- .../photo-sphere-viewer-adapter.svelte | 20 +- .../asset-viewer/photo-viewer.svelte | 80 +++-- .../asset-viewer/slideshow-bar.svelte | 56 ++-- .../asset-viewer/video-native-viewer.svelte | 77 +++-- .../asset-viewer/video-wrapper-viewer.svelte | 34 ++- .../lib/components/assets/broken-asset.svelte | 13 +- .../assets/thumbnail/image-thumbnail.svelte | 80 +++-- .../assets/thumbnail/thumbnail.svelte | 131 ++++---- .../assets/thumbnail/video-thumbnail.svelte | 70 +++-- web/src/lib/components/elements/badge.svelte | 15 +- .../components/elements/buttons/button.svelte | 103 +++---- .../buttons/circle-icon-button.svelte | 114 +++---- .../elements/buttons/link-button.svelte | 29 +- .../elements/buttons/skip-link.svelte | 22 +- .../lib/components/elements/checkbox.svelte | 30 +- .../lib/components/elements/date-input.svelte | 30 +- .../lib/components/elements/dropdown.svelte | 44 ++- .../lib/components/elements/group-tab.svelte | 14 +- web/src/lib/components/elements/icon.svelte | 51 +++- .../components/elements/radio-button.svelte | 14 +- .../lib/components/elements/search-bar.svelte | 33 ++- web/src/lib/components/elements/slider.svelte | 34 ++- web/src/lib/components/error.svelte | 8 +- .../faces-page/assign-face-side-panel.svelte | 42 +-- .../faces-page/edit-name-input.svelte | 36 ++- .../faces-page/face-thumbnail.svelte | 28 +- .../manage-people-visibility.svelte | 98 +++--- .../faces-page/merge-face-selector.svelte | 32 +- .../faces-page/merge-suggestion-modal.svelte | 44 ++- .../components/faces-page/people-card.svelte | 24 +- .../faces-page/people-infinite-scroll.svelte | 27 +- .../components/faces-page/people-list.svelte | 32 +- .../faces-page/people-search.svelte | 52 ++-- .../faces-page/person-side-panel.svelte | 48 +-- .../faces-page/set-birth-date-modal.svelte | 24 +- .../faces-page/unmerge-face-selector.svelte | 48 +-- .../forms/admin-registration-form.svelte | 23 +- .../lib/components/forms/api-key-form.svelte | 36 ++- .../components/forms/api-key-secret.svelte | 16 +- .../forms/change-password-form.svelte | 25 +- .../components/forms/create-user-form.svelte | 56 ++-- .../components/forms/edit-album-form.svelte | 32 +- .../components/forms/edit-user-form.svelte | 46 ++- .../library-exclusion-pattern-form.svelte | 48 ++- .../forms/library-import-path-form.svelte | 54 +++- .../forms/library-import-paths-form.svelte | 37 ++- .../forms/library-rename-form.svelte | 19 +- .../forms/library-scan-settings-form.svelte | 33 ++- .../forms/library-user-picker-form.svelte | 26 +- .../lib/components/forms/login-form.svelte | 31 +- .../components/forms/tag-asset-form.svelte | 42 ++- .../i18n/__test__/format-tag-b.svelte | 18 +- .../i18n/format-bold-message.svelte | 18 +- .../lib/components/i18n/format-message.svelte | 19 +- .../layouts/user-page-layout.svelte | 58 ++-- .../map-page/map-settings-modal.svelte | 34 ++- .../memory-page/memory-viewer.svelte | 91 +++--- .../onboarding-page/onboarding-card.svelte | 12 +- .../onboarding-page/onboarding-hello.svelte | 8 +- .../onboarding-page/onboarding-privacy.svelte | 79 ++--- .../onboarding-storage-template.svelte | 87 +++--- .../onboarding-page/onboarding-theme.svelte | 12 +- .../photos-page/actions/add-to-album.svelte | 10 +- .../photos-page/actions/archive-action.svelte | 19 +- .../actions/asset-job-actions.svelte | 14 +- .../actions/change-date-action.svelte | 8 +- .../actions/change-location-action.svelte | 8 +- .../actions/create-shared-link.svelte | 4 +- .../photos-page/actions/delete-assets.svelte | 20 +- .../actions/download-action.svelte | 12 +- .../actions/favorite-action.svelte | 19 +- .../actions/link-live-photo-action.svelte | 22 +- .../actions/remove-from-album.svelte | 12 +- .../actions/remove-from-shared-link.svelte | 8 +- .../photos-page/actions/restore-assets.svelte | 10 +- .../actions/select-all-assets.svelte | 12 +- .../photos-page/actions/stack-action.svelte | 10 +- .../photos-page/actions/tag-action.svelte | 14 +- .../components/photos-page/asset-grid.svelte | 279 ++++++++++-------- .../asset-select-control-bar.svelte | 28 +- .../photos-page/delete-asset-dialog.svelte | 22 +- .../photos-page/measure-date-group.svelte | 12 +- .../components/photos-page/memory-lane.svelte | 28 +- .../components/photos-page/skeleton.svelte | 8 +- .../individual-shared-viewer.svelte | 32 +- .../album-selection-modal.svelte | 38 +-- .../autogrow-textarea.svelte | 28 +- .../shared-components/change-date.spec.ts | 10 + .../shared-components/change-date.svelte | 64 ++-- .../shared-components/change-location.svelte | 196 ++++++------ .../shared-components/combobox.svelte | 82 ++--- .../context-menu/button-context-menu.svelte | 87 +++--- .../context-menu/context-menu.svelte | 49 ++- .../context-menu/menu-option.svelte | 35 ++- .../right-click-context-menu.svelte | 55 ++-- .../shared-components/control-app-bar.svelte | 42 ++- .../coordinates-input.svelte | 10 +- .../create-shared-link-modal.svelte | 66 +++-- .../dialog/confirm-dialog.svelte | 53 ++-- .../drag-and-drop-upload-overlay.svelte | 45 ++- .../empty-placeholder.svelte | 18 +- .../full-screen-modal.svelte | 68 +++-- .../fullscreen-container.svelte | 15 +- .../gallery-viewer/gallery-viewer.svelte | 68 +++-- .../help-and-feedback-modal.svelte | 7 +- .../immich-logo-small-link.svelte | 6 +- .../shared-components/immich-logo.svelte | 8 +- .../shared-components/loading-spinner.svelte | 6 +- .../shared-components/map/map.svelte | 229 +++++++------- .../shared-components/modal-header.svelte | 34 ++- .../navigation-bar/account-info-panel.svelte | 18 +- .../navigation-bar/avatar-selector.svelte | 12 +- .../navigation-bar/navigation-bar.svelte | 37 ++- .../navigation-loading-bar.svelte | 2 +- .../notification-component-test.svelte | 6 +- .../notification/notification-card.svelte | 25 +- .../number-range-input.svelte | 49 ++- .../shared-components/password-field.svelte | 14 +- .../shared-components/portal/portal.svelte | 19 +- .../profile-image-cropper.svelte | 25 +- .../progress-bar/progress-bar.svelte | 58 ++-- .../purchase-activation-success.svelte | 8 +- .../purchasing/purchase-content.svelte | 15 +- .../purchasing/purchase-modal.svelte | 8 +- .../scrubber/scrubber.svelte | 79 +++-- .../search-bar/search-bar.svelte | 89 +++--- .../search-bar/search-camera-section.svelte | 25 +- .../search-bar/search-date-section.svelte | 8 +- .../search-bar/search-display-section.svelte | 8 +- .../search-bar/search-filter-modal.svelte | 35 ++- .../search-bar/search-history-box.svelte | 57 ++-- .../search-bar/search-location-section.svelte | 31 +- .../search-bar/search-media-section.svelte | 6 +- .../search-bar/search-people-section.svelte | 16 +- .../search-bar/search-text-section.svelte | 8 +- .../server-about-modal.svelte | 9 +- .../settings/setting-accordion-state.svelte | 34 ++- .../settings/setting-accordion.svelte | 50 +++- .../settings/setting-buttons-row.svelte | 18 +- .../settings/setting-checkboxes.svelte | 28 +- .../settings/setting-combobox.svelte | 31 +- .../settings/setting-dropdown.svelte | 27 +- .../settings/setting-input-field.spec.ts | 4 +- .../settings/setting-input-field.svelte | 77 ++--- .../settings/setting-select.svelte | 34 ++- .../settings/setting-switch.svelte | 32 +- .../settings/setting-textarea.svelte | 36 ++- .../shared-components/show-shortcuts.svelte | 42 +-- .../side-bar/more-information-albums.svelte | 6 +- .../side-bar/more-information-assets.svelte | 6 +- .../side-bar/purchase-info.svelte | 54 ++-- .../side-bar/server-status.svelte | 19 +- .../side-bar/side-bar-link.svelte | 45 ++- .../side-bar/side-bar-section.svelte | 9 +- .../side-bar/side-bar.svelte | 40 +-- .../side-bar/storage-space.svelte | 18 +- .../side-bar/supporter-badge.svelte | 8 +- .../shared-components/single-grid-row.svelte | 24 +- .../shared-components/star-rating.svelte | 38 ++- .../shared-components/theme-button.svelte | 17 +- .../shared-components/tree/breadcrumbs.svelte | 16 +- .../tree/tree-item-thumbnails.svelte | 12 +- .../shared-components/tree/tree-items.svelte | 16 +- .../shared-components/tree/tree.svelte | 42 +-- .../upload-asset-preview.svelte | 12 +- .../shared-components/upload-panel.svelte | 30 +- .../shared-components/user-avatar.svelte | 59 ++-- .../version-announcement-box.svelte | 43 +-- .../actions/shared-link-copy.svelte | 10 +- .../actions/shared-link-delete.svelte | 10 +- .../actions/shared-link-edit.svelte | 10 +- .../covers/asset-cover.svelte | 17 +- .../sharedlinks-page/covers/no-cover.svelte | 11 +- .../covers/share-cover.svelte | 11 +- .../sharedlinks-page/shared-link-card.svelte | 14 +- .../lib/components/slideshow-settings.svelte | 22 +- .../user-settings-page/app-settings.svelte | 42 +-- .../change-password-settings.svelte | 19 +- .../user-settings-page/device-card.svelte | 10 +- .../user-settings-page/device-list.svelte | 12 +- .../download-settings.svelte | 19 +- .../feature-settings.svelte | 24 +- .../notifications-settings.svelte | 14 +- .../user-settings-page/oauth-settings.svelte | 12 +- .../partner-selection-modal.svelte | 18 +- .../partner-settings.svelte | 14 +- .../user-api-key-list.svelte | 18 +- .../user-profile-settings.svelte | 14 +- .../user-purchase-settings.svelte | 9 +- .../user-settings-list.svelte | 8 +- .../duplicates/duplicate-asset.svelte | 20 +- .../duplicates-compare-control.svelte | 29 +- web/src/lib/constants.ts | 27 ++ web/src/routes/(user)/+layout.svelte | 14 +- web/src/routes/(user)/albums/+page.svelte | 22 +- .../[[assetId=id]]/+page.svelte | 236 +++++++-------- .../[[assetId=id]]/+page.svelte | 12 +- web/src/routes/(user)/buy/+page.svelte | 8 +- web/src/routes/(user)/explore/+page.svelte | 73 +++-- .../[[assetId=id]]/+page.svelte | 12 +- .../[[assetId=id]]/+page.svelte | 48 +-- .../[[assetId=id]]/+page.svelte | 22 +- .../[[assetId=id]]/+page.svelte | 10 +- web/src/routes/(user)/people/+page.svelte | 113 ++++--- .../[[assetId=id]]/+page.svelte | 145 +++++---- .../(user)/photos/[[assetId=id]]/+page.svelte | 17 +- web/src/routes/(user)/places/+page.svelte | 12 +- .../[[assetId=id]]/+page.svelte | 119 ++++---- .../[[assetId=id]]/+page.svelte | 32 +- web/src/routes/(user)/sharing/+page.svelte | 40 ++- .../(user)/sharing/sharedlinks/+page.svelte | 8 +- .../[[assetId=id]]/+page.svelte | 142 +++++---- .../[[assetId=id]]/+page.svelte | 40 ++- .../routes/(user)/user-settings/+page.svelte | 14 +- web/src/routes/(user)/utilities/+page.svelte | 6 +- .../[[assetId=id]]/+page.svelte | 80 ++--- web/src/routes/+layout.svelte | 30 +- web/src/routes/admin/jobs-status/+page.svelte | 69 +++-- .../admin/library-management/+page.svelte | 68 +++-- web/src/routes/admin/repair/+page.svelte | 83 +++--- .../routes/admin/server-status/+page.svelte | 6 +- .../routes/admin/system-settings/+page.svelte | 122 ++++---- .../routes/admin/user-management/+page.svelte | 42 +-- .../routes/auth/change-password/+page.svelte | 20 +- web/src/routes/auth/login/+page.svelte | 16 +- web/src/routes/auth/onboarding/+page.svelte | 16 +- web/src/routes/auth/register/+page.svelte | 14 +- 310 files changed, 6435 insertions(+), 4176 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 63e7c05ca4..e9f26bee80 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -36,7 +36,7 @@ "@faker-js/faker": "^9.0.0", "@socket.io/component-emitter": "^3.1.0", "@sveltejs/adapter-static": "^3.0.5", - "@sveltejs/enhanced-img": "^0.3.0", + "@sveltejs/enhanced-img": "^0.3.9", "@sveltejs/kit": "^2.7.2", "@sveltejs/vite-plugin-svelte": "^4.0.0", "@testing-library/jest-dom": "^6.4.2", @@ -53,7 +53,7 @@ "dotenv": "^16.4.5", "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.43.0", + "eslint-plugin-svelte": "^2.45.1", "eslint-plugin-unicorn": "^55.0.0", "factory.ts": "^1.4.1", "globals": "^15.9.0", @@ -68,7 +68,7 @@ "tailwindcss": "^3.4.1", "tslib": "^2.6.2", "typescript": "^5.5.0", - "vite": "^5.1.4", + "vite": "^5.4.4", "vitest": "^2.0.5" } }, diff --git a/web/package.json b/web/package.json index af5e87c57e..c0c600f5bc 100644 --- a/web/package.json +++ b/web/package.json @@ -28,7 +28,7 @@ "@faker-js/faker": "^9.0.0", "@socket.io/component-emitter": "^3.1.0", "@sveltejs/adapter-static": "^3.0.5", - "@sveltejs/enhanced-img": "^0.3.0", + "@sveltejs/enhanced-img": "^0.3.9", "@sveltejs/kit": "^2.7.2", "@sveltejs/vite-plugin-svelte": "^4.0.0", "@testing-library/jest-dom": "^6.4.2", @@ -45,7 +45,7 @@ "dotenv": "^16.4.5", "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.43.0", + "eslint-plugin-svelte": "^2.45.1", "eslint-plugin-unicorn": "^55.0.0", "factory.ts": "^1.4.1", "globals": "^15.9.0", @@ -60,7 +60,7 @@ "tailwindcss": "^3.4.1", "tslib": "^2.6.2", "typescript": "^5.5.0", - "vite": "^5.1.4", + "vite": "^5.4.4", "vitest": "^2.0.5" }, "type": "module", diff --git a/web/src/lib/actions/__test__/focus-trap-test.svelte b/web/src/lib/actions/__test__/focus-trap-test.svelte index 207c880cd9..e1cb6fa4fb 100644 --- a/web/src/lib/actions/__test__/focus-trap-test.svelte +++ b/web/src/lib/actions/__test__/focus-trap-test.svelte @@ -1,16 +1,20 @@ - + {#if show}
text - +
diff --git a/web/src/lib/actions/autogrow.ts b/web/src/lib/actions/autogrow.ts index ff80454ef3..664039cb2a 100644 --- a/web/src/lib/actions/autogrow.ts +++ b/web/src/lib/actions/autogrow.ts @@ -1,4 +1,4 @@ -export const autoGrowHeight = (textarea: HTMLTextAreaElement, height = 'auto') => { +export const autoGrowHeight = (textarea?: HTMLTextAreaElement, height = 'auto') => { if (!textarea) { return; } diff --git a/web/src/lib/actions/context-menu-navigation.ts b/web/src/lib/actions/context-menu-navigation.ts index 3b45e7fe52..89b7b76d24 100644 --- a/web/src/lib/actions/context-menu-navigation.ts +++ b/web/src/lib/actions/context-menu-navigation.ts @@ -10,7 +10,7 @@ interface Options { /** * The container element that with direct children that should be navigated. */ - container: HTMLElement; + container?: HTMLElement; /** * Indicates if the dropdown is open. */ @@ -52,7 +52,11 @@ export const contextMenuNavigation: Action = (node, option await tick(); } - const children = Array.from(container?.children).filter((child) => child.tagName !== 'HR') as HTMLElement[]; + if (!container) { + return; + } + + const children = Array.from(container.children).filter((child) => child.tagName !== 'HR') as HTMLElement[]; if (children.length === 0) { return; } diff --git a/web/src/lib/actions/list-navigation.ts b/web/src/lib/actions/list-navigation.ts index 8f8ed62ed0..cd4214f700 100644 --- a/web/src/lib/actions/list-navigation.ts +++ b/web/src/lib/actions/list-navigation.ts @@ -6,8 +6,15 @@ import type { Action } from 'svelte/action'; * @param node Element which listens for keyboard events * @param container Element containing the list of elements */ -export const listNavigation: Action = (node, container: HTMLElement) => { +export const listNavigation: Action = ( + node: HTMLElement, + container?: HTMLElement, +) => { const moveFocus = (direction: 'up' | 'down') => { + if (!container) { + return; + } + const children = Array.from(container?.children); if (children.length === 0) { return; diff --git a/web/src/lib/components/admin-page/delete-confirm-dialogue.svelte b/web/src/lib/components/admin-page/delete-confirm-dialogue.svelte index a2fbbe787a..6eb603263e 100644 --- a/web/src/lib/components/admin-page/delete-confirm-dialogue.svelte +++ b/web/src/lib/components/admin-page/delete-confirm-dialogue.svelte @@ -7,13 +7,17 @@ import { deleteUserAdmin, type UserResponseDto } from '@immich/sdk'; import { t } from 'svelte-i18n'; - export let user: UserResponseDto; - export let onSuccess: () => void; - export let onFail: () => void; - export let onCancel: () => void; + interface Props { + user: UserResponseDto; + onSuccess: () => void; + onFail: () => void; + onCancel: () => void; + } - let forceDelete = false; - let deleteButtonDisabled = false; + let { user, onSuccess, onFail, onCancel }: Props = $props(); + + let forceDelete = $state(false); + let deleteButtonDisabled = $state(false); let userIdInput: string = ''; const handleDeleteUser = async () => { @@ -47,12 +51,14 @@ {onCancel} disabled={deleteButtonDisabled} > - + {#snippet promptSnippet()}
{#if forceDelete}

- - {message} + + {#snippet children({ message })} + {message} + {/snippet}

{:else} @@ -60,9 +66,10 @@ - {message} + {#snippet children({ message })} + {message} + {/snippet}

{/if} @@ -73,7 +80,7 @@ label={$t('admin.user_delete_immediately_checkbox')} labelClass="text-sm dark:text-immich-dark-fg" bind:checked={forceDelete} - on:change={() => { + onchange={() => { deleteButtonDisabled = forceDelete; }} /> @@ -92,9 +99,9 @@ aria-describedby="confirm-user-desc" name="confirm-user-id" type="text" - on:input={handleConfirm} + oninput={handleConfirm} /> {/if}
-
+ {/snippet} diff --git a/web/src/lib/components/admin-page/jobs/job-tile-button.svelte b/web/src/lib/components/admin-page/jobs/job-tile-button.svelte index 69d3706230..f71d8a3e44 100644 --- a/web/src/lib/components/admin-page/jobs/job-tile-button.svelte +++ b/web/src/lib/components/admin-page/jobs/job-tile-button.svelte @@ -1,10 +1,18 @@ -
- + {@render children?.()}
diff --git a/web/src/lib/components/admin-page/jobs/job-tile.svelte b/web/src/lib/components/admin-page/jobs/job-tile.svelte index 81c23e927b..0e39647c75 100644 --- a/web/src/lib/components/admin-page/jobs/job-tile.svelte +++ b/web/src/lib/components/admin-page/jobs/job-tile.svelte @@ -19,22 +19,37 @@ import JobTileButton from './job-tile-button.svelte'; import JobTileStatus from './job-tile-status.svelte'; - export let title: string; - export let subtitle: string | undefined; - export let description: Component | undefined; - export let jobCounts: JobCountsDto; - export let queueStatus: QueueStatusDto; - export let icon: string; - export let disabled = false; + interface Props { + title: string; + subtitle: string | undefined; + description: Component | undefined; + jobCounts: JobCountsDto; + queueStatus: QueueStatusDto; + icon: string; + disabled?: boolean; + allText: string | undefined; + refreshText: string | undefined; + missingText: string; + onCommand: (command: JobCommandDto) => void; + } - export let allText: string | undefined; - export let refreshText: string | undefined; - export let missingText: string; - export let onCommand: (command: JobCommandDto) => void; + let { + title, + subtitle, + description, + jobCounts, + queueStatus, + icon, + disabled = false, + allText, + refreshText, + missingText, + onCommand, + }: Props = $props(); - $: waitingCount = jobCounts.waiting + jobCounts.paused + jobCounts.delayed; - $: isIdle = !queueStatus.isActive && !queueStatus.isPaused; - $: multipleButtons = allText || refreshText; + let waitingCount = $derived(jobCounts.waiting + jobCounts.paused + jobCounts.delayed); + let isIdle = $derived(!queueStatus.isActive && !queueStatus.isPaused); + let multipleButtons = $derived(allText || refreshText); const commonClasses = 'flex place-items-center justify-between w-full py-2 sm:py-4 pr-4 pl-6'; @@ -67,7 +82,7 @@ title={$t('clear_message')} size="12" padding="1" - on:click={() => onCommand({ command: JobCommand.ClearFailed, force: false })} + onclick={() => onCommand({ command: JobCommand.ClearFailed, force: false })} />
@@ -87,8 +102,9 @@ {/if} {#if description} + {@const SvelteComponent = description}
- +
{/if} @@ -118,7 +134,7 @@ onCommand({ command: JobCommand.Start, force: false })} + onClick={() => onCommand({ command: JobCommand.Start, force: false })} > {$t('disabled').toUpperCase()} @@ -127,20 +143,20 @@ {#if !disabled && !isIdle} {#if waitingCount > 0} - onCommand({ command: JobCommand.Empty, force: false })}> + onCommand({ command: JobCommand.Empty, force: false })}> {$t('clear').toUpperCase()} {/if} {#if queueStatus.isPaused} {@const size = waitingCount > 0 ? '24' : '48'} - onCommand({ command: JobCommand.Resume, force: false })}> + onCommand({ command: JobCommand.Resume, force: false })}> {$t('resume').toUpperCase()} {:else} - onCommand({ command: JobCommand.Pause, force: false })}> + onCommand({ command: JobCommand.Pause, force: false })}> {$t('pause').toUpperCase()} @@ -149,25 +165,25 @@ {#if !disabled && multipleButtons && isIdle} {#if allText} - onCommand({ command: JobCommand.Start, force: true })}> + onCommand({ command: JobCommand.Start, force: true })}> {allText} {/if} {#if refreshText} - onCommand({ command: JobCommand.Start, force: undefined })}> + onCommand({ command: JobCommand.Start, force: undefined })}> {refreshText} {/if} - onCommand({ command: JobCommand.Start, force: false })}> + onCommand({ command: JobCommand.Start, force: false })}> {missingText} {/if} {#if !disabled && !multipleButtons && isIdle} - onCommand({ command: JobCommand.Start, force: false })}> + onCommand({ command: JobCommand.Start, force: false })}> {$t('start').toUpperCase()} diff --git a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte index 67d672d398..9b4f3ffdd6 100644 --- a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte +++ b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte @@ -25,7 +25,11 @@ import { dialogController } from '$lib/components/shared-components/dialog/dialog'; import { t } from 'svelte-i18n'; - export let jobs: AllJobStatusResponseDto; + interface Props { + jobs: AllJobStatusResponseDto; + } + + let { jobs = $bindable() }: Props = $props(); interface JobDetails { title: string; @@ -56,8 +60,7 @@ await handleCommand(jobId, dto); }; - // svelte-ignore reactive_declaration_non_reactive_property - $: jobDetails = >>{ + let jobDetails: Partial> = { [JobName.ThumbnailGeneration]: { icon: mdiFileJpgBox, title: $getJobName(JobName.ThumbnailGeneration), @@ -142,7 +145,8 @@ missingText: $t('missing'), }, }; - $: jobList = Object.entries(jobDetails) as [JobName, JobDetails][]; + + let jobList = Object.entries(jobDetails) as [JobName, JobDetails][]; async function handleCommand(jobId: JobName, jobCommand: JobCommandDto) { const title = jobDetails[jobId]?.title; diff --git a/web/src/lib/components/admin-page/jobs/storage-migration-description.svelte b/web/src/lib/components/admin-page/jobs/storage-migration-description.svelte index 8a74d2c5ad..b47df1daae 100644 --- a/web/src/lib/components/admin-page/jobs/storage-migration-description.svelte +++ b/web/src/lib/components/admin-page/jobs/storage-migration-description.svelte @@ -7,12 +7,13 @@ - - {message} - + {#snippet children({ message })} + + {message} + + {/snippet} diff --git a/web/src/lib/components/admin-page/restore-dialogue.svelte b/web/src/lib/components/admin-page/restore-dialogue.svelte index 25afbc6d4b..a72ada2ca5 100644 --- a/web/src/lib/components/admin-page/restore-dialogue.svelte +++ b/web/src/lib/components/admin-page/restore-dialogue.svelte @@ -5,10 +5,14 @@ import { restoreUserAdmin, type UserResponseDto } from '@immich/sdk'; import { t } from 'svelte-i18n'; - export let user: UserResponseDto; - export let onSuccess: () => void; - export let onFail: () => void; - export let onCancel: () => void; + interface Props { + user: UserResponseDto; + onSuccess: () => void; + onFail: () => void; + onCancel: () => void; + } + + let { user, onSuccess, onFail, onCancel }: Props = $props(); const handleRestoreUser = async () => { try { @@ -32,11 +36,13 @@ onConfirm={handleRestoreUser} {onCancel} > - + {#snippet promptSnippet()}

- - {message} + + {#snippet children({ message })} + {message} + {/snippet}

-
+ {/snippet} diff --git a/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte b/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte index 35afc0962d..feab6a9c6d 100644 --- a/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte +++ b/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte @@ -7,14 +7,20 @@ import StatsCard from './stats-card.svelte'; import { t } from 'svelte-i18n'; - export let stats: ServerStatsResponseDto = { - photos: 0, - videos: 0, - usage: 0, - usageByUser: [], - }; + interface Props { + stats?: ServerStatsResponseDto; + } - $: zeros = (value: number) => { + let { + stats = { + photos: 0, + videos: 0, + usage: 0, + usageByUser: [], + }, + }: Props = $props(); + + const zeros = (value: number) => { const maxLength = 13; const valueLength = value.toString().length; const zeroLength = maxLength - valueLength; @@ -23,7 +29,7 @@ }; const TiB = 1024 ** 4; - $: [statsUsage, statsUsageUnit] = getBytesWithUnit(stats.usage, stats.usage > TiB ? 2 : 0); + let [statsUsage, statsUsageUnit] = $derived(getBytesWithUnit(stats.usage, stats.usage > TiB ? 2 : 0));
diff --git a/web/src/lib/components/admin-page/server-stats/stats-card.svelte b/web/src/lib/components/admin-page/server-stats/stats-card.svelte index 31baa0afdd..14d32c055f 100644 --- a/web/src/lib/components/admin-page/server-stats/stats-card.svelte +++ b/web/src/lib/components/admin-page/server-stats/stats-card.svelte @@ -2,18 +2,22 @@ import Icon from '$lib/components/elements/icon.svelte'; import { ByteUnit } from '$lib/utils/byte-units'; - export let icon: string; - export let title: string; - export let value: number; - export let unit: ByteUnit | undefined = undefined; + interface Props { + icon: string; + title: string; + value: number; + unit?: ByteUnit | undefined; + } - $: zeros = () => { + let { icon, title, value, unit = undefined }: Props = $props(); + + const zeros = $derived(() => { const maxLength = 13; const valueLength = value.toString().length; const zeroLength = maxLength - valueLength; return '0'.repeat(zeroLength); - }; + });
diff --git a/web/src/lib/components/admin-page/settings/admin-settings.svelte b/web/src/lib/components/admin-page/settings/admin-settings.svelte index 19a8580d6b..199db0b571 100644 --- a/web/src/lib/components/admin-page/settings/admin-settings.svelte +++ b/web/src/lib/components/admin-page/settings/admin-settings.svelte @@ -1,5 +1,3 @@ - - {#if savedConfig && defaultConfig} - + {@render children({ savedConfig, defaultConfig })} {/if} diff --git a/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte b/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte index 9b0e4b3270..7f94dfa253 100644 --- a/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte +++ b/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte @@ -2,9 +2,7 @@ import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte'; import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte'; import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte'; - import SettingInputField, { - SettingInputFieldType, - } from '$lib/components/shared-components/settings/setting-input-field.svelte'; + import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import { type SystemConfigDto } from '@immich/sdk'; import { isEqual } from 'lodash-es'; @@ -12,15 +10,20 @@ import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings'; import { t } from 'svelte-i18n'; import FormatMessage from '$lib/components/i18n/format-message.svelte'; + import { SettingInputFieldType } from '$lib/constants'; - export let savedConfig: SystemConfigDto; - export let defaultConfig: SystemConfigDto; - export let config: SystemConfigDto; // this is the config that is being edited - export let disabled = false; - export let onReset: SettingsResetEvent; - export let onSave: SettingsSaveEvent; + interface Props { + savedConfig: SystemConfigDto; + defaultConfig: SystemConfigDto; + config: SystemConfigDto; + disabled?: boolean; + onReset: SettingsResetEvent; + onSave: SettingsSaveEvent; + } - let isConfirmOpen = false; + let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props(); + + let isConfirmOpen = $state(false); const handleToggleOverride = () => { // click runs before bind @@ -48,29 +51,31 @@ onCancel={() => (isConfirmOpen = false)} onConfirm={() => handleSave(true)} > - + {#snippet promptSnippet()}

{$t('admin.authentication_settings_disable_all')}

- - - {message} - + + {#snippet children({ message })} + + {message} + + {/snippet}

-
+ {/snippet} {/if}
-
+ e.preventDefault()}>

- - - {message} - + + {#snippet children({ message })} + + {message} + + {/snippet}

@@ -147,7 +154,7 @@ handleToggleOverride()} + onToggle={() => handleToggleOverride()} bind:checked={config.oauth.mobileOverrideEnabled} /> diff --git a/web/src/lib/components/admin-page/settings/backup-settings/backup-settings.svelte b/web/src/lib/components/admin-page/settings/backup-settings/backup-settings.svelte index 05543f1124..3ec477e29c 100644 --- a/web/src/lib/components/admin-page/settings/backup-settings/backup-settings.svelte +++ b/web/src/lib/components/admin-page/settings/backup-settings/backup-settings.svelte @@ -3,33 +3,40 @@ import { isEqual } from 'lodash-es'; import { fade } from 'svelte/transition'; import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings'; - import SettingInputField, { - SettingInputFieldType, - } from '$lib/components/shared-components/settings/setting-input-field.svelte'; + import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte'; import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte'; import { t } from 'svelte-i18n'; import FormatMessage from '$lib/components/i18n/format-message.svelte'; + import { SettingInputFieldType } from '$lib/constants'; - export let savedConfig: SystemConfigDto; - export let defaultConfig: SystemConfigDto; - export let config: SystemConfigDto; // this is the config that is being edited - export let disabled = false; - export let onReset: SettingsResetEvent; - export let onSave: SettingsSaveEvent; + interface Props { + savedConfig: SystemConfigDto; + defaultConfig: SystemConfigDto; + config: SystemConfigDto; + disabled?: boolean; + onReset: SettingsResetEvent; + onSave: SettingsSaveEvent; + } - $: cronExpressionOptions = [ + let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props(); + + let cronExpressionOptions = $derived([ { text: $t('interval.night_at_midnight'), value: '0 0 * * *' }, { text: $t('interval.night_at_twoam'), value: '0 02 * * *' }, { text: $t('interval.day_at_onepm'), value: '0 13 * * *' }, { text: $t('interval.hours', { values: { hours: 6 } }), value: '0 */6 * * *' }, - ]; + ]); + + const onsubmit = (event: Event) => { + event.preventDefault(); + };
- +
- + {#snippet descriptionSnippet()}

- - - {message} -
-
+ + {#snippet children({ message })} + + {message} +
+
+ {/snippet}

-
+ {/snippet} { + event.preventDefault(); + };
- +

- - {#if tag === 'h264-link'} - - {message} - - {:else if tag === 'hevc-link'} - - {message} - - {:else if tag === 'vp9-link'} - - {message} - - {/if} + + {#snippet children({ tag, message })} + {#if tag === 'h264-link'} + + {message} + + {:else if tag === 'hevc-link'} + + {message} + + {:else if tag === 'vp9-link'} + + {message} + + {/if} + {/snippet}

@@ -60,7 +69,7 @@ inputType={SettingInputFieldType.NUMBER} {disabled} label={$t('admin.transcoding_constant_rate_factor')} - desc={$t('admin.transcoding_constant_rate_factor_description')} + description={$t('admin.transcoding_constant_rate_factor_description')} bind:value={config.ffmpeg.crf} required={true} isEdited={config.ffmpeg.crf !== savedConfig.ffmpeg.crf} @@ -186,7 +195,7 @@ inputType={SettingInputFieldType.TEXT} {disabled} label={$t('admin.transcoding_max_bitrate')} - desc={$t('admin.transcoding_max_bitrate_description')} + description={$t('admin.transcoding_max_bitrate_description')} bind:value={config.ffmpeg.maxBitrate} isEdited={config.ffmpeg.maxBitrate !== savedConfig.ffmpeg.maxBitrate} /> @@ -195,7 +204,7 @@ inputType={SettingInputFieldType.NUMBER} {disabled} label={$t('admin.transcoding_threads')} - desc={$t('admin.transcoding_threads_description')} + description={$t('admin.transcoding_threads_description')} bind:value={config.ffmpeg.threads} isEdited={config.ffmpeg.threads !== savedConfig.ffmpeg.threads} /> @@ -329,7 +338,7 @@ { + event.preventDefault(); + };
- +
{ + event.preventDefault(); + };
- + {#each jobNames as jobName}
{#if isSystemConfigJobDto(jobName)} @@ -46,7 +53,7 @@ inputType={SettingInputFieldType.NUMBER} {disabled} label={$t('admin.job_concurrency', { values: { job: $getJobName(jobName) } })} - desc="" + description="" bind:value={config.job[jobName].concurrency} required={true} isEdited={!(config.job[jobName].concurrency == savedConfig.job[jobName].concurrency)} @@ -55,7 +62,7 @@ { + event.preventDefault(); + };
- +
- + {#snippet descriptionSnippet()}

- - - {message} - + + {#snippet children({ message })} + + {message} + + {/snippet}

-
+ {/snippet}
diff --git a/web/src/lib/components/admin-page/settings/logging-settings/logging-settings.svelte b/web/src/lib/components/admin-page/settings/logging-settings/logging-settings.svelte index 6e71ba926c..29a1c65162 100644 --- a/web/src/lib/components/admin-page/settings/logging-settings/logging-settings.svelte +++ b/web/src/lib/components/admin-page/settings/logging-settings/logging-settings.svelte @@ -8,17 +8,25 @@ import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte'; import { t } from 'svelte-i18n'; - export let savedConfig: SystemConfigDto; - export let defaultConfig: SystemConfigDto; - export let config: SystemConfigDto; // this is the config that is being edited - export let disabled = false; - export let onReset: SettingsResetEvent; - export let onSave: SettingsSaveEvent; + interface Props { + savedConfig: SystemConfigDto; + defaultConfig: SystemConfigDto; + config: SystemConfigDto; + disabled?: boolean; + onReset: SettingsResetEvent; + onSave: SettingsSaveEvent; + } + + let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props(); + + const onsubmit = (event: Event) => { + event.preventDefault(); + };
- +
{ + event.preventDefault(); + };
- +
-

- - {message} - -

+ {#snippet descriptionSnippet()} +

+ + {#snippet children({ message })} + {message} + {/snippet} + +

+ {/snippet}
@@ -100,7 +111,7 @@ step="0.0005" min={0.001} max={0.1} - desc={$t('admin.machine_learning_max_detection_distance_description')} + description={$t('admin.machine_learning_max_detection_distance_description')} disabled={disabled || !$featureFlags.duplicateDetection} isEdited={config.machineLearning.duplicateDetection.maxDistance !== savedConfig.machineLearning.duplicateDetection.maxDistance} @@ -142,7 +153,7 @@ { + event.preventDefault(); + };
- +
@@ -38,7 +45,7 @@ - + {#snippet subtitleSnippet()}

- - - {message} - + + {#snippet children({ message })} + + {message} + + {/snippet}

-
+ {/snippet}
{ + event.preventDefault(); + };
- +
{ + event.preventDefault(); + };
- +
{ if (isSending) { @@ -65,11 +68,15 @@ isSending = false; } }; + + const onsubmit = (event: Event) => { + event.preventDefault(); + };
- +
@@ -85,7 +92,7 @@ inputType={SettingInputFieldType.TEXT} required label={$t('host')} - desc={$t('admin.notification_email_host_description')} + description={$t('admin.notification_email_host_description')} disabled={disabled || !config.notifications.smtp.enabled} bind:value={config.notifications.smtp.transport.host} isEdited={config.notifications.smtp.transport.host !== savedConfig.notifications.smtp.transport.host} @@ -95,7 +102,7 @@ inputType={SettingInputFieldType.NUMBER} required label={$t('port')} - desc={$t('admin.notification_email_port_description')} + description={$t('admin.notification_email_port_description')} disabled={disabled || !config.notifications.smtp.enabled} bind:value={config.notifications.smtp.transport.port} isEdited={config.notifications.smtp.transport.port !== savedConfig.notifications.smtp.transport.port} @@ -104,7 +111,7 @@
-
{/if} diff --git a/web/src/lib/components/album-page/album-cover.svelte b/web/src/lib/components/album-page/album-cover.svelte index d0444f3599..3f71bbe632 100644 --- a/web/src/lib/components/album-page/album-cover.svelte +++ b/web/src/lib/components/album-page/album-cover.svelte @@ -5,13 +5,18 @@ import AssetCover from '$lib/components/sharedlinks-page/covers/asset-cover.svelte'; import { t } from 'svelte-i18n'; - export let album: AlbumResponseDto; - export let preload = false; - let className = ''; - export { className as class }; + interface Props { + album: AlbumResponseDto; + preload?: boolean; + class?: string; + } - $: alt = album.albumName || $t('unnamed_album'); - $: thumbnailUrl = album.albumThumbnailAssetId ? getAssetThumbnailUrl({ id: album.albumThumbnailAssetId }) : null; + let { album, preload = false, class: className = '' }: Props = $props(); + + let alt = $derived(album.albumName || $t('unnamed_album')); + let thumbnailUrl = $derived( + album.albumThumbnailAssetId ? getAssetThumbnailUrl({ id: album.albumThumbnailAssetId }) : null, + ); {#if thumbnailUrl} diff --git a/web/src/lib/components/album-page/album-description.svelte b/web/src/lib/components/album-page/album-description.svelte index b3ad688a30..46b424f93a 100644 --- a/web/src/lib/components/album-page/album-description.svelte +++ b/web/src/lib/components/album-page/album-description.svelte @@ -4,9 +4,13 @@ import AutogrowTextarea from '$lib/components/shared-components/autogrow-textarea.svelte'; import { t } from 'svelte-i18n'; - export let id: string; - export let description: string; - export let isOwned: boolean; + interface Props { + id: string; + description: string; + isOwned: boolean; + } + + let { id, description = $bindable(), isOwned }: Props = $props(); const handleUpdateDescription = async (newDescription: string) => { try { diff --git a/web/src/lib/components/album-page/album-options.svelte b/web/src/lib/components/album-page/album-options.svelte index 3ec1842757..884de8c2a2 100644 --- a/web/src/lib/components/album-page/album-options.svelte +++ b/web/src/lib/components/album-page/album-options.svelte @@ -23,24 +23,38 @@ import { notificationController, NotificationType } from '../shared-components/notification/notification'; import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; - export let album: AlbumResponseDto; - export let order: AssetOrder | undefined; - export let user: UserResponseDto; // Declare user as a prop - export let onChangeOrder: (order: AssetOrder) => void; - export let onClose: () => void; - export let onToggleEnabledActivity: () => void; - export let onShowSelectSharedUser: () => void; - export let onRemove: (userId: string) => void; - export let onRefreshAlbum: () => void; + interface Props { + album: AlbumResponseDto; + order: AssetOrder | undefined; + user: UserResponseDto; + onChangeOrder: (order: AssetOrder) => void; + onClose: () => void; + onToggleEnabledActivity: () => void; + onShowSelectSharedUser: () => void; + onRemove: (userId: string) => void; + onRefreshAlbum: () => void; + } - let selectedRemoveUser: UserResponseDto | null = null; + let { + album, + order, + user, + onChangeOrder, + onClose, + onToggleEnabledActivity, + onShowSelectSharedUser, + onRemove, + onRefreshAlbum, + }: Props = $props(); + + let selectedRemoveUser: UserResponseDto | null = $state(null); const options: Record = { [AssetOrder.Asc]: { icon: mdiArrowUpThin, title: $t('oldest_first') }, [AssetOrder.Desc]: { icon: mdiArrowDownThin, title: $t('newest_first') }, }; - $: selectedOption = order ? options[order] : options[AssetOrder.Desc]; + let selectedOption = $derived(order ? options[order] : options[AssetOrder.Desc]); const handleToggle = async (returnedOption: RenderedOption): Promise => { if (selectedOption === returnedOption) { @@ -125,7 +139,7 @@
{$t('people').toUpperCase()}
-
- createAlbumAndRedirect()}> + createAlbumAndRedirect()}>
@@ -184,7 +164,7 @@