diff --git a/mobile/assets/i18n/de-DE.json b/mobile/assets/i18n/de-DE.json index 5aeedf37fc10b..b8cb59c00da97 100644 --- a/mobile/assets/i18n/de-DE.json +++ b/mobile/assets/i18n/de-DE.json @@ -391,6 +391,7 @@ "setting_image_viewer_original_title": "Original laden", "setting_image_viewer_preview_subtitle": "Aktivieren, um ein Bild mit mittlerer Auflösung zu laden. Deaktivieren, um entweder das Original direkt zu laden oder nur die Miniaturansicht zu verwenden.", "setting_image_viewer_preview_title": "Vorschaubild laden", + "setting_image_viewer_title": "Bilder", "setting_languages_apply": "Anwenden", "setting_languages_title": "Sprachen", "setting_notifications_notify_failures_grace_period": "Benachrichtigung über Fehler bei der Hintergrundsicherung: {}", @@ -406,6 +407,9 @@ "setting_notifications_total_progress_subtitle": "Gesamter Upload-Fortschritt (abgeschlossen/Anzahl Elemente)", "setting_notifications_total_progress_title": "Zeige Gesamtfortschritt bei der Hintergrundsicherung", "setting_pages_app_bar_settings": "Einstellungen", + "setting_video_viewer_looping_subtitle": "Aktivieren, damit sich ein Video in der Detailansicht automatisch wiederholt.", + "setting_video_viewer_looping_title": "Wiederholen", + "setting_video_viewer_title": "Videos", "settings_require_restart": "Bitte starte Immich neu, um diese Einstellung anzuwenden.", "share_add": "Hinzufügen", "share_add_photos": "Fotos hinzufügen", diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index c968c603f75bb..8a986c6f997fd 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -391,6 +391,7 @@ "setting_image_viewer_original_title": "Load original image", "setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.", "setting_image_viewer_preview_title": "Load preview image", + "setting_image_viewer_title": "Images", "setting_languages_apply": "Apply", "setting_languages_title": "Languages", "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}", @@ -406,6 +407,9 @@ "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)", "setting_notifications_total_progress_title": "Show background backup total progress", "setting_pages_app_bar_settings": "Settings", + "setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.", + "setting_video_viewer_looping_title": "Looping", + "setting_video_viewer_title": "Videos", "settings_require_restart": "Please restart Immich to apply this setting", "share_add": "Add", "share_add_photos": "Add photos", diff --git a/mobile/lib/entities/store.entity.dart b/mobile/lib/entities/store.entity.dart index 053fdc4e3cee5..e7f1a6bce0c33 100644 --- a/mobile/lib/entities/store.entity.dart +++ b/mobile/lib/entities/store.entity.dart @@ -182,6 +182,7 @@ enum StoreKey { advancedTroubleshooting(114, type: bool), logLevel(115, type: int), preferRemoteImage(116, type: bool), + loopVideo(117, type: bool), // map related settings mapShowFavoriteOnly(118, type: bool), mapRelativeDate(119, type: int), diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart index 14fe8b02a4f31..505d339aba0d3 100644 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ b/mobile/lib/pages/common/gallery_viewer.page.dart @@ -60,6 +60,7 @@ class GalleryViewerPage extends HookConsumerWidget { final settings = ref.watch(appSettingsServiceProvider); final isLoadPreview = useState(AppSettingsEnum.loadPreview.defaultValue); final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue); + final shouldLoopVideo = useState(AppSettingsEnum.loopVideo.defaultValue); final isZoomed = useState(false); final isPlayingVideo = useState(false); final localPosition = useState(null); @@ -102,6 +103,8 @@ class GalleryViewerPage extends HookConsumerWidget { settings.getSetting(AppSettingsEnum.loadPreview); isLoadOriginal.value = settings.getSetting(AppSettingsEnum.loadOriginal); + shouldLoopVideo.value = + settings.getSetting(AppSettingsEnum.loopVideo); return null; }, [], @@ -368,6 +371,7 @@ class GalleryViewerPage extends HookConsumerWidget { key: ValueKey(a), asset: a, isMotionVideo: a.livePhotoVideoId != null, + loopVideo: shouldLoopVideo.value, placeholder: Image( image: provider, fit: BoxFit.contain, diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index e20b06c24a02a..486eeba4cd4bd 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -5,8 +5,8 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/widgets/settings/advanced_settings.dart'; import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_settings.dart'; +import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart'; -import 'package:immich_mobile/widgets/settings/image_viewer_quality_setting.dart'; import 'package:immich_mobile/widgets/settings/language_settings.dart'; import 'package:immich_mobile/widgets/settings/notification_setting.dart'; import 'package:immich_mobile/widgets/settings/preference_settings/preference_setting.dart'; @@ -33,7 +33,7 @@ enum SettingSection { SettingSection.preferences => const PreferenceSetting(), SettingSection.backup => const BackupSettings(), SettingSection.timeline => const AssetListSettings(), - SettingSection.viewer => const ImageViewerQualitySetting(), + SettingSection.viewer => const AssetViewerSettings(), SettingSection.advanced => const AdvancedSettings(), }; diff --git a/mobile/lib/pages/common/video_viewer.page.dart b/mobile/lib/pages/common/video_viewer.page.dart index 853f013f5d65a..4c6f7344f79ab 100644 --- a/mobile/lib/pages/common/video_viewer.page.dart +++ b/mobile/lib/pages/common/video_viewer.page.dart @@ -17,6 +17,7 @@ class VideoViewerPage extends HookConsumerWidget { final Duration hideControlsTimer; final bool showControls; final bool showDownloadingIndicator; + final bool loopVideo; const VideoViewerPage({ super.key, @@ -26,6 +27,7 @@ class VideoViewerPage extends HookConsumerWidget { this.showControls = true, this.hideControlsTimer = const Duration(seconds: 5), this.showDownloadingIndicator = true, + this.loopVideo = false, }); @override @@ -73,7 +75,9 @@ class VideoViewerPage extends HookConsumerWidget { // Also sets the error if there is an error in the playback void updateVideoPlayback() { final videoPlayback = VideoPlaybackValue.fromController(controller); - ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback; + if (!loopVideo) { + ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback; + } final state = videoPlayback.state; // Enable the WakeLock while the video is playing @@ -153,6 +157,7 @@ class VideoViewerPage extends HookConsumerWidget { hideControlsTimer: hideControlsTimer, showControls: showControls, showDownloadingIndicator: showDownloadingIndicator, + loopVideo: loopVideo, ), ), ], diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index a803a6807f294..fd6c2d89a79ac 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -46,6 +46,7 @@ enum AppSettingsEnum { advancedTroubleshooting(StoreKey.advancedTroubleshooting, null, false), logLevel(StoreKey.logLevel, null, 5), // Level.INFO = 5 preferRemoteImage(StoreKey.preferRemoteImage, null, false), + loopVideo(StoreKey.loopVideo, "loopVideo", true), mapThemeMode(StoreKey.mapThemeMode, null, 0), mapShowFavoriteOnly(StoreKey.mapShowFavoriteOnly, null, false), mapIncludeArchived(StoreKey.mapIncludeArchived, null, false), diff --git a/mobile/lib/utils/hooks/chewiew_controller_hook.dart b/mobile/lib/utils/hooks/chewiew_controller_hook.dart index 5daeb389ec44d..08c30b2770456 100644 --- a/mobile/lib/utils/hooks/chewiew_controller_hook.dart +++ b/mobile/lib/utils/hooks/chewiew_controller_hook.dart @@ -17,6 +17,7 @@ ChewieController useChewieController({ bool allowFullScreen = false, bool allowedScreenSleep = false, bool showControls = true, + bool loopVideo = false, Widget? customControls, Widget? placeholder, Duration hideControlsTimer = const Duration(seconds: 1), @@ -36,6 +37,7 @@ ChewieController useChewieController({ hideControlsTimer: hideControlsTimer, showControlsOnInitialize: showControlsOnInitialize, showControls: showControls, + loopVideo: loopVideo, allowedScreenSleep: allowedScreenSleep, onPlaying: onPlaying, onPaused: onPaused, @@ -53,6 +55,7 @@ class _ChewieControllerHook extends Hook { final bool allowFullScreen; final bool allowedScreenSleep; final bool showControls; + final bool loopVideo; final Widget? customControls; final Widget? placeholder; final Duration hideControlsTimer; @@ -71,6 +74,7 @@ class _ChewieControllerHook extends Hook { this.allowFullScreen = false, this.allowedScreenSleep = false, this.showControls = true, + this.loopVideo = false, this.customControls, this.placeholder, this.hideControlsTimer = const Duration(seconds: 3), @@ -94,6 +98,7 @@ class _ChewieControllerHookState allowFullScreen: hook.allowFullScreen, allowedScreenSleep: hook.allowedScreenSleep, showControls: hook.showControls, + looping: hook.loopVideo, customControls: hook.customControls, placeholder: hook.placeholder, hideControlsTimer: hook.hideControlsTimer, diff --git a/mobile/lib/widgets/asset_viewer/video_player.dart b/mobile/lib/widgets/asset_viewer/video_player.dart index 076eafdca3a65..ebf158b59a5fb 100644 --- a/mobile/lib/widgets/asset_viewer/video_player.dart +++ b/mobile/lib/widgets/asset_viewer/video_player.dart @@ -12,6 +12,7 @@ class VideoPlayerViewer extends HookConsumerWidget { final Duration hideControlsTimer; final bool showControls; final bool showDownloadingIndicator; + final bool loopVideo; const VideoPlayerViewer({ super.key, @@ -21,6 +22,7 @@ class VideoPlayerViewer extends HookConsumerWidget { required this.hideControlsTimer, required this.showControls, required this.showDownloadingIndicator, + required this.loopVideo, }); @override @@ -36,6 +38,7 @@ class VideoPlayerViewer extends HookConsumerWidget { ), showControls: showControls && !isMotionVideo, hideControlsTimer: hideControlsTimer, + loopVideo: loopVideo, ); return Chewie( 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 new file mode 100644 index 0000000000000..23dca85b6ffbb --- /dev/null +++ b/mobile/lib/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart'; +import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; +import 'video_viewer_settings.dart'; + +class AssetViewerSettings extends StatelessWidget { + const AssetViewerSettings({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final assetViewerSetting = [ + const ImageViewerQualitySetting(), + const VideoViewerSettings(), + ]; + + return SettingsSubPageScaffold( + settings: assetViewerSetting, + showDivider: true, + ); + } +} 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 new file mode 100644 index 0000000000000..444977d9a2844 --- /dev/null +++ b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart @@ -0,0 +1,47 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; +import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; +import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; + +class ImageViewerQualitySetting extends HookConsumerWidget { + const ImageViewerQualitySetting({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isPreview = useAppSettingsState(AppSettingsEnum.loadPreview); + final isOriginal = useAppSettingsState(AppSettingsEnum.loadOriginal); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SettingsSubTitle(title: "setting_image_viewer_title".tr()), + ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 20), + title: Text( + 'setting_image_viewer_help', + style: context.textTheme.bodyMedium, + ).tr(), + ), + SettingsSwitchListTile( + valueNotifier: isPreview, + title: "setting_image_viewer_preview_title".tr(), + subtitle: "setting_image_viewer_preview_subtitle".tr(), + onChanged: (_) => ref.invalidate(appSettingsServiceProvider), + ), + SettingsSwitchListTile( + valueNotifier: isOriginal, + title: "setting_image_viewer_original_title".tr(), + subtitle: "setting_image_viewer_original_subtitle".tr(), + onChanged: (_) => ref.invalidate(appSettingsServiceProvider), + ), + ], + ); + } +} 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 new file mode 100644 index 0000000000000..e21c49bb06c61 --- /dev/null +++ b/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart @@ -0,0 +1,32 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; +import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; +import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; + +class VideoViewerSettings extends HookConsumerWidget { + const VideoViewerSettings({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final useLoopVideo = useAppSettingsState(AppSettingsEnum.loopVideo); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SettingsSubTitle(title: "setting_video_viewer_title".tr()), + SettingsSwitchListTile( + valueNotifier: useLoopVideo, + title: "setting_video_viewer_looping_title".tr(), + subtitle: "setting_video_viewer_looping_subtitle".tr(), + onChanged: (_) => ref.invalidate(appSettingsServiceProvider), + ), + ], + ); + } +} diff --git a/mobile/lib/widgets/settings/image_viewer_quality_setting.dart b/mobile/lib/widgets/settings/image_viewer_quality_setting.dart deleted file mode 100644 index 8c45db786366d..0000000000000 --- a/mobile/lib/widgets/settings/image_viewer_quality_setting.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; -import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; -import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; - -class ImageViewerQualitySetting extends HookWidget { - const ImageViewerQualitySetting({ - super.key, - }); - - @override - Widget build(BuildContext context) { - final isPreview = useAppSettingsState(AppSettingsEnum.loadPreview); - final isOriginal = useAppSettingsState(AppSettingsEnum.loadOriginal); - - final viewerSettings = [ - ListTile( - title: Text( - 'setting_image_viewer_help', - style: context.textTheme.bodyMedium, - ).tr(), - ), - SettingsSwitchListTile( - valueNotifier: isPreview, - title: "setting_image_viewer_preview_title".tr(), - subtitle: "setting_image_viewer_preview_subtitle".tr(), - ), - SettingsSwitchListTile( - valueNotifier: isOriginal, - title: "setting_image_viewer_original_title".tr(), - subtitle: "setting_image_viewer_original_subtitle".tr(), - ), - ]; - - return SettingsSubPageScaffold(settings: viewerSettings); - } -} diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 1b6d9479a9e95..01af94d71a8d8 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -634,6 +634,7 @@ navigateAsset()} on:onVideoStarted={handleVideoStarted} @@ -655,6 +656,7 @@ (shouldPlayMotionPhoto = false)} /> @@ -669,6 +671,7 @@ navigateAsset()} on:onVideoStarted={handleVideoStarted} diff --git a/web/src/lib/components/asset-viewer/video-native-viewer.svelte b/web/src/lib/components/asset-viewer/video-native-viewer.svelte index a0c3aa19af078..7863513bfb9be 100644 --- a/web/src/lib/components/asset-viewer/video-native-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-native-viewer.svelte @@ -1,5 +1,5 @@ {#if projectionType === ProjectionType.EQUIRECTANGULAR} {:else} - + {/if} diff --git a/web/src/lib/components/user-settings-page/app-settings.svelte b/web/src/lib/components/user-settings-page/app-settings.svelte index 83b19a46bdaa4..a9a1839cf67c1 100644 --- a/web/src/lib/components/user-settings-page/app-settings.svelte +++ b/web/src/lib/components/user-settings-page/app-settings.svelte @@ -3,9 +3,15 @@ import SettingCombobox from '$lib/components/shared-components/settings/setting-combobox.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import { fallbackLocale, locales } from '$lib/constants'; - import { sidebarSettings } from '$lib/stores/preferences.store'; - import { alwaysLoadOriginalFile, playVideoThumbnailOnHover, showDeleteModal } from '$lib/stores/preferences.store'; - import { colorTheme, locale } from '$lib/stores/preferences.store'; + import { + alwaysLoadOriginalFile, + colorTheme, + locale, + loopVideo, + playVideoThumbnailOnHover, + showDeleteModal, + sidebarSettings, + } from '$lib/stores/preferences.store'; import { findLocale } from '$lib/utils'; import { onMount } from 'svelte'; import { fade } from 'svelte/transition'; @@ -117,6 +123,15 @@ on:toggle={() => ($playVideoThumbnailOnHover = !$playVideoThumbnailOnHover)} /> +
+ ($loopVideo = !$loopVideo)} + /> +
('delete-confirm-dialog', true, export const alwaysLoadOriginalFile = persisted('always-load-original-file', false, {}); export const playVideoThumbnailOnHover = persisted('play-video-thumbnail-on-hover', true, {}); + +export const loopVideo = persisted('loop-video', true, {});