feat(web,mobile) Allow videos to be looped in the detail viewer (#8615)

* First version of video looping for the web

* Use prop for slideshow state

* refactor asset settings and add autoloop video setting

* rename variables and adjust description

* loop videos based on user settings in gallery viewer

* make asset viewer setting a stateless widget

* do not update video playback value if looping is enabled

* add some translations

* adjust description

* add missing id

* WIP

* chore: clean up

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
kleinMaggus 2024-05-14 21:31:47 +02:00 committed by GitHub
parent 0f129cae4a
commit d62e90424e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 160 additions and 49 deletions

View File

@ -391,6 +391,7 @@
"setting_image_viewer_original_title": "Original laden", "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_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_preview_title": "Vorschaubild laden",
"setting_image_viewer_title": "Bilder",
"setting_languages_apply": "Anwenden", "setting_languages_apply": "Anwenden",
"setting_languages_title": "Sprachen", "setting_languages_title": "Sprachen",
"setting_notifications_notify_failures_grace_period": "Benachrichtigung über Fehler bei der Hintergrundsicherung: {}", "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_subtitle": "Gesamter Upload-Fortschritt (abgeschlossen/Anzahl Elemente)",
"setting_notifications_total_progress_title": "Zeige Gesamtfortschritt bei der Hintergrundsicherung", "setting_notifications_total_progress_title": "Zeige Gesamtfortschritt bei der Hintergrundsicherung",
"setting_pages_app_bar_settings": "Einstellungen", "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.", "settings_require_restart": "Bitte starte Immich neu, um diese Einstellung anzuwenden.",
"share_add": "Hinzufügen", "share_add": "Hinzufügen",
"share_add_photos": "Fotos hinzufügen", "share_add_photos": "Fotos hinzufügen",

View File

@ -391,6 +391,7 @@
"setting_image_viewer_original_title": "Load original image", "setting_image_viewer_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_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_preview_title": "Load preview image",
"setting_image_viewer_title": "Images",
"setting_languages_apply": "Apply", "setting_languages_apply": "Apply",
"setting_languages_title": "Languages", "setting_languages_title": "Languages",
"setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}", "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_subtitle": "Overall upload progress (done/total assets)",
"setting_notifications_total_progress_title": "Show background backup total progress", "setting_notifications_total_progress_title": "Show background backup total progress",
"setting_pages_app_bar_settings": "Settings", "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", "settings_require_restart": "Please restart Immich to apply this setting",
"share_add": "Add", "share_add": "Add",
"share_add_photos": "Add photos", "share_add_photos": "Add photos",

View File

@ -182,6 +182,7 @@ enum StoreKey<T> {
advancedTroubleshooting<bool>(114, type: bool), advancedTroubleshooting<bool>(114, type: bool),
logLevel<int>(115, type: int), logLevel<int>(115, type: int),
preferRemoteImage<bool>(116, type: bool), preferRemoteImage<bool>(116, type: bool),
loopVideo<bool>(117, type: bool),
// map related settings // map related settings
mapShowFavoriteOnly<bool>(118, type: bool), mapShowFavoriteOnly<bool>(118, type: bool),
mapRelativeDate<int>(119, type: int), mapRelativeDate<int>(119, type: int),

View File

@ -60,6 +60,7 @@ class GalleryViewerPage extends HookConsumerWidget {
final settings = ref.watch(appSettingsServiceProvider); final settings = ref.watch(appSettingsServiceProvider);
final isLoadPreview = useState(AppSettingsEnum.loadPreview.defaultValue); final isLoadPreview = useState(AppSettingsEnum.loadPreview.defaultValue);
final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue); final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue);
final shouldLoopVideo = useState(AppSettingsEnum.loopVideo.defaultValue);
final isZoomed = useState(false); final isZoomed = useState(false);
final isPlayingVideo = useState(false); final isPlayingVideo = useState(false);
final localPosition = useState<Offset?>(null); final localPosition = useState<Offset?>(null);
@ -102,6 +103,8 @@ class GalleryViewerPage extends HookConsumerWidget {
settings.getSetting<bool>(AppSettingsEnum.loadPreview); settings.getSetting<bool>(AppSettingsEnum.loadPreview);
isLoadOriginal.value = isLoadOriginal.value =
settings.getSetting<bool>(AppSettingsEnum.loadOriginal); settings.getSetting<bool>(AppSettingsEnum.loadOriginal);
shouldLoopVideo.value =
settings.getSetting<bool>(AppSettingsEnum.loopVideo);
return null; return null;
}, },
[], [],
@ -368,6 +371,7 @@ class GalleryViewerPage extends HookConsumerWidget {
key: ValueKey(a), key: ValueKey(a),
asset: a, asset: a,
isMotionVideo: a.livePhotoVideoId != null, isMotionVideo: a.livePhotoVideoId != null,
loopVideo: shouldLoopVideo.value,
placeholder: Image( placeholder: Image(
image: provider, image: provider,
fit: BoxFit.contain, fit: BoxFit.contain,

View File

@ -5,8 +5,8 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/widgets/settings/advanced_settings.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_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/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/language_settings.dart';
import 'package:immich_mobile/widgets/settings/notification_setting.dart'; import 'package:immich_mobile/widgets/settings/notification_setting.dart';
import 'package:immich_mobile/widgets/settings/preference_settings/preference_setting.dart'; import 'package:immich_mobile/widgets/settings/preference_settings/preference_setting.dart';
@ -33,7 +33,7 @@ enum SettingSection {
SettingSection.preferences => const PreferenceSetting(), SettingSection.preferences => const PreferenceSetting(),
SettingSection.backup => const BackupSettings(), SettingSection.backup => const BackupSettings(),
SettingSection.timeline => const AssetListSettings(), SettingSection.timeline => const AssetListSettings(),
SettingSection.viewer => const ImageViewerQualitySetting(), SettingSection.viewer => const AssetViewerSettings(),
SettingSection.advanced => const AdvancedSettings(), SettingSection.advanced => const AdvancedSettings(),
}; };

View File

@ -17,6 +17,7 @@ class VideoViewerPage extends HookConsumerWidget {
final Duration hideControlsTimer; final Duration hideControlsTimer;
final bool showControls; final bool showControls;
final bool showDownloadingIndicator; final bool showDownloadingIndicator;
final bool loopVideo;
const VideoViewerPage({ const VideoViewerPage({
super.key, super.key,
@ -26,6 +27,7 @@ class VideoViewerPage extends HookConsumerWidget {
this.showControls = true, this.showControls = true,
this.hideControlsTimer = const Duration(seconds: 5), this.hideControlsTimer = const Duration(seconds: 5),
this.showDownloadingIndicator = true, this.showDownloadingIndicator = true,
this.loopVideo = false,
}); });
@override @override
@ -73,7 +75,9 @@ class VideoViewerPage extends HookConsumerWidget {
// Also sets the error if there is an error in the playback // Also sets the error if there is an error in the playback
void updateVideoPlayback() { void updateVideoPlayback() {
final videoPlayback = VideoPlaybackValue.fromController(controller); final videoPlayback = VideoPlaybackValue.fromController(controller);
ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback; if (!loopVideo) {
ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback;
}
final state = videoPlayback.state; final state = videoPlayback.state;
// Enable the WakeLock while the video is playing // Enable the WakeLock while the video is playing
@ -153,6 +157,7 @@ class VideoViewerPage extends HookConsumerWidget {
hideControlsTimer: hideControlsTimer, hideControlsTimer: hideControlsTimer,
showControls: showControls, showControls: showControls,
showDownloadingIndicator: showDownloadingIndicator, showDownloadingIndicator: showDownloadingIndicator,
loopVideo: loopVideo,
), ),
), ),
], ],

View File

@ -46,6 +46,7 @@ enum AppSettingsEnum<T> {
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false), advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
logLevel<int>(StoreKey.logLevel, null, 5), // Level.INFO = 5 logLevel<int>(StoreKey.logLevel, null, 5), // Level.INFO = 5
preferRemoteImage<bool>(StoreKey.preferRemoteImage, null, false), preferRemoteImage<bool>(StoreKey.preferRemoteImage, null, false),
loopVideo<bool>(StoreKey.loopVideo, "loopVideo", true),
mapThemeMode<int>(StoreKey.mapThemeMode, null, 0), mapThemeMode<int>(StoreKey.mapThemeMode, null, 0),
mapShowFavoriteOnly<bool>(StoreKey.mapShowFavoriteOnly, null, false), mapShowFavoriteOnly<bool>(StoreKey.mapShowFavoriteOnly, null, false),
mapIncludeArchived<bool>(StoreKey.mapIncludeArchived, null, false), mapIncludeArchived<bool>(StoreKey.mapIncludeArchived, null, false),

View File

@ -17,6 +17,7 @@ ChewieController useChewieController({
bool allowFullScreen = false, bool allowFullScreen = false,
bool allowedScreenSleep = false, bool allowedScreenSleep = false,
bool showControls = true, bool showControls = true,
bool loopVideo = false,
Widget? customControls, Widget? customControls,
Widget? placeholder, Widget? placeholder,
Duration hideControlsTimer = const Duration(seconds: 1), Duration hideControlsTimer = const Duration(seconds: 1),
@ -36,6 +37,7 @@ ChewieController useChewieController({
hideControlsTimer: hideControlsTimer, hideControlsTimer: hideControlsTimer,
showControlsOnInitialize: showControlsOnInitialize, showControlsOnInitialize: showControlsOnInitialize,
showControls: showControls, showControls: showControls,
loopVideo: loopVideo,
allowedScreenSleep: allowedScreenSleep, allowedScreenSleep: allowedScreenSleep,
onPlaying: onPlaying, onPlaying: onPlaying,
onPaused: onPaused, onPaused: onPaused,
@ -53,6 +55,7 @@ class _ChewieControllerHook extends Hook<ChewieController> {
final bool allowFullScreen; final bool allowFullScreen;
final bool allowedScreenSleep; final bool allowedScreenSleep;
final bool showControls; final bool showControls;
final bool loopVideo;
final Widget? customControls; final Widget? customControls;
final Widget? placeholder; final Widget? placeholder;
final Duration hideControlsTimer; final Duration hideControlsTimer;
@ -71,6 +74,7 @@ class _ChewieControllerHook extends Hook<ChewieController> {
this.allowFullScreen = false, this.allowFullScreen = false,
this.allowedScreenSleep = false, this.allowedScreenSleep = false,
this.showControls = true, this.showControls = true,
this.loopVideo = false,
this.customControls, this.customControls,
this.placeholder, this.placeholder,
this.hideControlsTimer = const Duration(seconds: 3), this.hideControlsTimer = const Duration(seconds: 3),
@ -94,6 +98,7 @@ class _ChewieControllerHookState
allowFullScreen: hook.allowFullScreen, allowFullScreen: hook.allowFullScreen,
allowedScreenSleep: hook.allowedScreenSleep, allowedScreenSleep: hook.allowedScreenSleep,
showControls: hook.showControls, showControls: hook.showControls,
looping: hook.loopVideo,
customControls: hook.customControls, customControls: hook.customControls,
placeholder: hook.placeholder, placeholder: hook.placeholder,
hideControlsTimer: hook.hideControlsTimer, hideControlsTimer: hook.hideControlsTimer,

View File

@ -12,6 +12,7 @@ class VideoPlayerViewer extends HookConsumerWidget {
final Duration hideControlsTimer; final Duration hideControlsTimer;
final bool showControls; final bool showControls;
final bool showDownloadingIndicator; final bool showDownloadingIndicator;
final bool loopVideo;
const VideoPlayerViewer({ const VideoPlayerViewer({
super.key, super.key,
@ -21,6 +22,7 @@ class VideoPlayerViewer extends HookConsumerWidget {
required this.hideControlsTimer, required this.hideControlsTimer,
required this.showControls, required this.showControls,
required this.showDownloadingIndicator, required this.showDownloadingIndicator,
required this.loopVideo,
}); });
@override @override
@ -36,6 +38,7 @@ class VideoPlayerViewer extends HookConsumerWidget {
), ),
showControls: showControls && !isMotionVideo, showControls: showControls && !isMotionVideo,
hideControlsTimer: hideControlsTimer, hideControlsTimer: hideControlsTimer,
loopVideo: loopVideo,
); );
return Chewie( return Chewie(

View File

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
import 'video_viewer_settings.dart';
class AssetViewerSettings extends StatelessWidget {
const AssetViewerSettings({
super.key,
});
@override
Widget build(BuildContext context) {
final assetViewerSetting = [
const ImageViewerQualitySetting(),
const VideoViewerSettings(),
];
return SettingsSubPageScaffold(
settings: assetViewerSetting,
showDivider: true,
);
}
}

View File

@ -0,0 +1,47 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
class ImageViewerQualitySetting extends HookConsumerWidget {
const ImageViewerQualitySetting({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isPreview = useAppSettingsState(AppSettingsEnum.loadPreview);
final isOriginal = useAppSettingsState(AppSettingsEnum.loadOriginal);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SettingsSubTitle(title: "setting_image_viewer_title".tr()),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
title: Text(
'setting_image_viewer_help',
style: context.textTheme.bodyMedium,
).tr(),
),
SettingsSwitchListTile(
valueNotifier: isPreview,
title: "setting_image_viewer_preview_title".tr(),
subtitle: "setting_image_viewer_preview_subtitle".tr(),
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
),
SettingsSwitchListTile(
valueNotifier: isOriginal,
title: "setting_image_viewer_original_title".tr(),
subtitle: "setting_image_viewer_original_subtitle".tr(),
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
),
],
);
}
}

View File

@ -0,0 +1,32 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
class VideoViewerSettings extends HookConsumerWidget {
const VideoViewerSettings({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final useLoopVideo = useAppSettingsState(AppSettingsEnum.loopVideo);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SettingsSubTitle(title: "setting_video_viewer_title".tr()),
SettingsSwitchListTile(
valueNotifier: useLoopVideo,
title: "setting_video_viewer_looping_title".tr(),
subtitle: "setting_video_viewer_looping_subtitle".tr(),
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
),
],
);
}
}

View File

@ -1,41 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
class ImageViewerQualitySetting extends HookWidget {
const ImageViewerQualitySetting({
super.key,
});
@override
Widget build(BuildContext context) {
final isPreview = useAppSettingsState(AppSettingsEnum.loadPreview);
final isOriginal = useAppSettingsState(AppSettingsEnum.loadOriginal);
final viewerSettings = [
ListTile(
title: Text(
'setting_image_viewer_help',
style: context.textTheme.bodyMedium,
).tr(),
),
SettingsSwitchListTile(
valueNotifier: isPreview,
title: "setting_image_viewer_preview_title".tr(),
subtitle: "setting_image_viewer_preview_subtitle".tr(),
),
SettingsSwitchListTile(
valueNotifier: isOriginal,
title: "setting_image_viewer_original_title".tr(),
subtitle: "setting_image_viewer_original_subtitle".tr(),
),
];
return SettingsSubPageScaffold(settings: viewerSettings);
}
}

View File

@ -634,6 +634,7 @@
<VideoViewer <VideoViewer
assetId={previewStackedAsset.id} assetId={previewStackedAsset.id}
projectionType={previewStackedAsset.exifInfo?.projectionType} projectionType={previewStackedAsset.exifInfo?.projectionType}
loopVideo={true}
on:close={closeViewer} on:close={closeViewer}
on:onVideoEnded={() => navigateAsset()} on:onVideoEnded={() => navigateAsset()}
on:onVideoStarted={handleVideoStarted} on:onVideoStarted={handleVideoStarted}
@ -655,6 +656,7 @@
<VideoViewer <VideoViewer
assetId={asset.livePhotoVideoId} assetId={asset.livePhotoVideoId}
projectionType={asset.exifInfo?.projectionType} projectionType={asset.exifInfo?.projectionType}
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
on:close={closeViewer} on:close={closeViewer}
on:onVideoEnded={() => (shouldPlayMotionPhoto = false)} on:onVideoEnded={() => (shouldPlayMotionPhoto = false)}
/> />
@ -669,6 +671,7 @@
<VideoViewer <VideoViewer
assetId={asset.id} assetId={asset.id}
projectionType={asset.exifInfo?.projectionType} projectionType={asset.exifInfo?.projectionType}
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
on:close={closeViewer} on:close={closeViewer}
on:onVideoEnded={() => navigateAsset()} on:onVideoEnded={() => navigateAsset()}
on:onVideoStarted={handleVideoStarted} on:onVideoStarted={handleVideoStarted}

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { videoViewerVolume, videoViewerMuted } from '$lib/stores/preferences.store'; import { loopVideo as loopVideoPreference, videoViewerVolume, videoViewerMuted } from '$lib/stores/preferences.store';
import { getAssetFileUrl, getAssetThumbnailUrl } from '$lib/utils'; import { getAssetFileUrl, getAssetThumbnailUrl } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { ThumbnailFormat } from '@immich/sdk'; import { ThumbnailFormat } from '@immich/sdk';
@ -8,6 +8,7 @@
import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import LoadingSpinner from '../shared-components/loading-spinner.svelte';
export let assetId: string; export let assetId: string;
export let loopVideo: boolean;
let element: HTMLVideoElement | undefined = undefined; let element: HTMLVideoElement | undefined = undefined;
let isVideoLoading = true; let isVideoLoading = true;
@ -34,6 +35,7 @@
> >
<video <video
bind:this={element} bind:this={element}
loop={$loopVideoPreference && loopVideo}
autoplay autoplay
playsinline playsinline
controls controls

View File

@ -6,10 +6,11 @@
export let assetId: string; export let assetId: string;
export let projectionType: string | null | undefined; export let projectionType: string | null | undefined;
export let loopVideo: boolean;
</script> </script>
{#if projectionType === ProjectionType.EQUIRECTANGULAR} {#if projectionType === ProjectionType.EQUIRECTANGULAR}
<PanoramaViewer asset={{ id: assetId, type: AssetTypeEnum.Video }} /> <PanoramaViewer asset={{ id: assetId, type: AssetTypeEnum.Video }} />
{:else} {:else}
<VideoNativeViewer {assetId} on:onVideoEnded on:onVideoStarted /> <VideoNativeViewer {loopVideo} {assetId} on:onVideoEnded on:onVideoStarted />
{/if} {/if}

View File

@ -3,9 +3,15 @@
import SettingCombobox from '$lib/components/shared-components/settings/setting-combobox.svelte'; import SettingCombobox from '$lib/components/shared-components/settings/setting-combobox.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { fallbackLocale, locales } from '$lib/constants'; import { fallbackLocale, locales } from '$lib/constants';
import { sidebarSettings } from '$lib/stores/preferences.store'; import {
import { alwaysLoadOriginalFile, playVideoThumbnailOnHover, showDeleteModal } from '$lib/stores/preferences.store'; alwaysLoadOriginalFile,
import { colorTheme, locale } from '$lib/stores/preferences.store'; colorTheme,
locale,
loopVideo,
playVideoThumbnailOnHover,
showDeleteModal,
sidebarSettings,
} from '$lib/stores/preferences.store';
import { findLocale } from '$lib/utils'; import { findLocale } from '$lib/utils';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
@ -117,6 +123,15 @@
on:toggle={() => ($playVideoThumbnailOnHover = !$playVideoThumbnailOnHover)} on:toggle={() => ($playVideoThumbnailOnHover = !$playVideoThumbnailOnHover)}
/> />
</div> </div>
<div class="ml-4">
<SettingSwitch
id="loop-video"
title="Loop videos"
subtitle="Enable to automatically loop a video in the detail viewer."
bind:checked={$loopVideo}
on:toggle={() => ($loopVideo = !$loopVideo)}
/>
</div>
<div class="ml-4"> <div class="ml-4">
<SettingSwitch <SettingSwitch

View File

@ -138,3 +138,5 @@ export const showDeleteModal = persisted<boolean>('delete-confirm-dialog', true,
export const alwaysLoadOriginalFile = persisted<boolean>('always-load-original-file', false, {}); export const alwaysLoadOriginalFile = persisted<boolean>('always-load-original-file', false, {});
export const playVideoThumbnailOnHover = persisted<boolean>('play-video-thumbnail-on-hover', true, {}); export const playVideoThumbnailOnHover = persisted<boolean>('play-video-thumbnail-on-hover', true, {});
export const loopVideo = persisted<boolean>('loop-video', true, {});