mirror of
https://github.com/immich-app/immich.git
synced 2025-06-03 05:34:32 -04:00
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:
parent
0f129cae4a
commit
d62e90424e
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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),
|
||||||
|
@ -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,
|
||||||
|
@ -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(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -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),
|
||||||
|
@ -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,
|
||||||
|
@ -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(
|
||||||
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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}
|
||||||
|
@ -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
|
||||||
|
@ -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}
|
||||||
|
@ -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
|
||||||
|
@ -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, {});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user