Skip to content

Commit

Permalink
feat(web,mobile) Allow videos to be looped in the detail viewer (#8615)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
3 people committed May 14, 2024
1 parent 0f129ca commit d62e904
Show file tree
Hide file tree
Showing 18 changed files with 160 additions and 49 deletions.
4 changes: 4 additions & 0 deletions mobile/assets/i18n/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -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: {}",
Expand All @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions mobile/assets/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -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: {}",
Expand All @@ -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",
Expand Down
1 change: 1 addition & 0 deletions mobile/lib/entities/store.entity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ enum StoreKey<T> {
advancedTroubleshooting<bool>(114, type: bool),
logLevel<int>(115, type: int),
preferRemoteImage<bool>(116, type: bool),
loopVideo<bool>(117, type: bool),
// map related settings
mapShowFavoriteOnly<bool>(118, type: bool),
mapRelativeDate<int>(119, type: int),
Expand Down
4 changes: 4 additions & 0 deletions mobile/lib/pages/common/gallery_viewer.page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Offset?>(null);
Expand Down Expand Up @@ -102,6 +103,8 @@ class GalleryViewerPage extends HookConsumerWidget {
settings.getSetting<bool>(AppSettingsEnum.loadPreview);
isLoadOriginal.value =
settings.getSetting<bool>(AppSettingsEnum.loadOriginal);
shouldLoopVideo.value =
settings.getSetting<bool>(AppSettingsEnum.loopVideo);
return null;
},
[],
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions mobile/lib/pages/common/settings.page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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(),
};

Expand Down
7 changes: 6 additions & 1 deletion mobile/lib/pages/common/video_viewer.page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class VideoViewerPage extends HookConsumerWidget {
final Duration hideControlsTimer;
final bool showControls;
final bool showDownloadingIndicator;
final bool loopVideo;

const VideoViewerPage({
super.key,
Expand All @@ -26,6 +27,7 @@ class VideoViewerPage extends HookConsumerWidget {
this.showControls = true,
this.hideControlsTimer = const Duration(seconds: 5),
this.showDownloadingIndicator = true,
this.loopVideo = false,
});

@override
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -153,6 +157,7 @@ class VideoViewerPage extends HookConsumerWidget {
hideControlsTimer: hideControlsTimer,
showControls: showControls,
showDownloadingIndicator: showDownloadingIndicator,
loopVideo: loopVideo,
),
),
],
Expand Down
1 change: 1 addition & 0 deletions mobile/lib/services/app_settings.service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ enum AppSettingsEnum<T> {
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
logLevel<int>(StoreKey.logLevel, null, 5), // Level.INFO = 5
preferRemoteImage<bool>(StoreKey.preferRemoteImage, null, false),
loopVideo<bool>(StoreKey.loopVideo, "loopVideo", true),
mapThemeMode<int>(StoreKey.mapThemeMode, null, 0),
mapShowFavoriteOnly<bool>(StoreKey.mapShowFavoriteOnly, null, false),
mapIncludeArchived<bool>(StoreKey.mapIncludeArchived, null, false),
Expand Down
5 changes: 5 additions & 0 deletions mobile/lib/utils/hooks/chewiew_controller_hook.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -36,6 +37,7 @@ ChewieController useChewieController({
hideControlsTimer: hideControlsTimer,
showControlsOnInitialize: showControlsOnInitialize,
showControls: showControls,
loopVideo: loopVideo,
allowedScreenSleep: allowedScreenSleep,
onPlaying: onPlaying,
onPaused: onPaused,
Expand All @@ -53,6 +55,7 @@ class _ChewieControllerHook extends Hook<ChewieController> {
final bool allowFullScreen;
final bool allowedScreenSleep;
final bool showControls;
final bool loopVideo;
final Widget? customControls;
final Widget? placeholder;
final Duration hideControlsTimer;
Expand All @@ -71,6 +74,7 @@ class _ChewieControllerHook extends Hook<ChewieController> {
this.allowFullScreen = false,
this.allowedScreenSleep = false,
this.showControls = true,
this.loopVideo = false,
this.customControls,
this.placeholder,
this.hideControlsTimer = const Duration(seconds: 3),
Expand All @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions mobile/lib/widgets/asset_viewer/video_player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class VideoPlayerViewer extends HookConsumerWidget {
final Duration hideControlsTimer;
final bool showControls;
final bool showDownloadingIndicator;
final bool loopVideo;

const VideoPlayerViewer({
super.key,
Expand All @@ -21,6 +22,7 @@ class VideoPlayerViewer extends HookConsumerWidget {
required this.hideControlsTimer,
required this.showControls,
required this.showDownloadingIndicator,
required this.loopVideo,
});

@override
Expand All @@ -36,6 +38,7 @@ class VideoPlayerViewer extends HookConsumerWidget {
),
showControls: showControls && !isMotionVideo,
hideControlsTimer: hideControlsTimer,
loopVideo: loopVideo,
);

return Chewie(
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
);
}
}
Original file line number Diff line number Diff line change
@@ -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),
),
],
);
}
}
Original file line number Diff line number Diff line change
@@ -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),
),
],
);
}
}
41 changes: 0 additions & 41 deletions mobile/lib/widgets/settings/image_viewer_quality_setting.dart

This file was deleted.

3 changes: 3 additions & 0 deletions web/src/lib/components/asset-viewer/asset-viewer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,7 @@
<VideoViewer
assetId={previewStackedAsset.id}
projectionType={previewStackedAsset.exifInfo?.projectionType}
loopVideo={true}
on:close={closeViewer}
on:onVideoEnded={() => navigateAsset()}
on:onVideoStarted={handleVideoStarted}
Expand All @@ -655,6 +656,7 @@
<VideoViewer
assetId={asset.livePhotoVideoId}
projectionType={asset.exifInfo?.projectionType}
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
on:close={closeViewer}
on:onVideoEnded={() => (shouldPlayMotionPhoto = false)}
/>
Expand All @@ -669,6 +671,7 @@
<VideoViewer
assetId={asset.id}
projectionType={asset.exifInfo?.projectionType}
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
on:close={closeViewer}
on:onVideoEnded={() => navigateAsset()}
on:onVideoStarted={handleVideoStarted}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<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 { handleError } from '$lib/utils/handle-error';
import { ThumbnailFormat } from '@immich/sdk';
Expand All @@ -8,6 +8,7 @@
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
export let assetId: string;
export let loopVideo: boolean;
let element: HTMLVideoElement | undefined = undefined;
let isVideoLoading = true;
Expand All @@ -34,6 +35,7 @@
>
<video
bind:this={element}
loop={$loopVideoPreference && loopVideo}
autoplay
playsinline
controls
Expand Down

0 comments on commit d62e904

Please sign in to comment.