From 128f19efa5c84bc02a17b8bb831b2a4a7f4685ea Mon Sep 17 00:00:00 2001 From: mertalev <101130780+mertalev@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:21:38 -0500 Subject: [PATCH] fix live photo play button not updating --- mobile/lib/constants/immich_colors.dart | 1 + .../lib/pages/common/gallery_viewer.page.dart | 11 ++-- .../common/native_video_viewer.page.dart | 63 +++++++------------ .../is_motion_video_playing.provider.dart | 23 +++++++ mobile/lib/routing/router.gr.dart | 6 -- .../widgets/asset_viewer/gallery_app_bar.dart | 10 +-- .../asset_viewer/motion_photo_button.dart | 22 +++++++ .../asset_viewer/top_control_app_bar.dart | 28 +-------- 8 files changed, 78 insertions(+), 86 deletions(-) create mode 100644 mobile/lib/providers/asset_viewer/is_motion_video_playing.provider.dart create mode 100644 mobile/lib/widgets/asset_viewer/motion_photo_button.dart diff --git a/mobile/lib/constants/immich_colors.dart b/mobile/lib/constants/immich_colors.dart index 8e8b8f46a3..847887de8c 100644 --- a/mobile/lib/constants/immich_colors.dart +++ b/mobile/lib/constants/immich_colors.dart @@ -21,6 +21,7 @@ const Color immichBrandColorLight = Color(0xFF4150AF); const Color immichBrandColorDark = Color(0xFFACCBFA); const Color whiteOpacity75 = Color.fromARGB((0.75 * 255) ~/ 1, 255, 255, 255); const Color red400 = Color(0xFFEF5350); +const Color grey200 = Color(0xFFEEEEEE); final Map _themePresetsMap = { ImmichColorPreset.indigo: ImmichTheme( diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart index aaa050f289..939b581372 100644 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ b/mobile/lib/pages/common/gallery_viewer.page.dart @@ -17,6 +17,7 @@ import 'package:immich_mobile/pages/common/gallery_stacked_children.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; @@ -55,7 +56,6 @@ class GalleryViewerPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final totalAssets = useState(renderList.totalAssets); final isZoomed = useState(false); - final isPlayingMotionVideo = useValueNotifier(false); final stackIndex = useState(0); final localPosition = useRef(null); final currentIndex = useValueNotifier(initialIndex); @@ -192,7 +192,7 @@ class GalleryViewerPage extends HookConsumerWidget { }, onLongPressStart: asset.isMotionPhoto ? (_, __, ___) { - isPlayingMotionVideo.value = true; + ref.read(isPlayingMotionVideoProvider.notifier).playing = true; } : null, imageProvider: ImmichImage.imageProvider(asset: asset), @@ -226,7 +226,6 @@ class GalleryViewerPage extends HookConsumerWidget { child: NativeVideoViewerPage( key: key, asset: asset, - isPlayingMotionVideo: isPlayingMotionVideo, image: Image( key: ValueKey(asset), image: ImmichImage.imageProvider( @@ -245,7 +244,7 @@ class GalleryViewerPage extends HookConsumerWidget { } PhotoViewGalleryPageOptions buildAsset(BuildContext context, int index) { - isPlayingMotionVideo.value = false; + ref.read(isPlayingMotionVideoProvider.notifier).playing = false; var newAsset = loadAsset(index); final stackId = newAsset.stackId; if (stackId != null && currentIndex.value == index) { @@ -278,7 +277,7 @@ class GalleryViewerPage extends HookConsumerWidget { return; } - if (asset.isImage && !isPlayingMotionVideo.value) { + if (asset.isImage && !ref.read(isPlayingMotionVideoProvider)) { isZoomed.value = state != PhotoViewScaleState.initial; ref.read(showControlsProvider.notifier).show = !isZoomed.value; @@ -324,7 +323,6 @@ class GalleryViewerPage extends HookConsumerWidget { currentIndex.value = value; stackIndex.value = 0; - isPlayingMotionVideo.value = false; ref.read(currentAssetProvider.notifier).set(newAsset); if (newAsset.isVideo || newAsset.isMotionPhoto) { @@ -345,7 +343,6 @@ class GalleryViewerPage extends HookConsumerWidget { child: GalleryAppBar( key: const ValueKey('app-bar'), showInfo: showInfo, - isPlayingMotionVideo: isPlayingMotionVideo, ), ), Positioned( diff --git a/mobile/lib/pages/common/native_video_viewer.page.dart b/mobile/lib/pages/common/native_video_viewer.page.dart index c07fac0bf0..6487ba5794 100644 --- a/mobile/lib/pages/common/native_video_viewer.page.dart +++ b/mobile/lib/pages/common/native_video_viewer.page.dart @@ -8,6 +8,7 @@ import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.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/services/api.service.dart'; @@ -26,15 +27,10 @@ class NativeVideoViewerPage extends HookConsumerWidget { final bool showControls; final Widget image; - /// Whether to display the video part of the motion photo - /// TODO: this should probably be a provider - final ValueNotifier? isPlayingMotionVideo; - const NativeVideoViewerPage({ super.key, required this.asset, required this.image, - this.isPlayingMotionVideo, this.showControls = true, }); @@ -48,10 +44,7 @@ class NativeVideoViewerPage extends HookConsumerWidget { final controller = useState(null); final lastVideoPosition = useRef(-1); final isBuffering = useRef(false); - - useListenable(isPlayingMotionVideo); - final showMotionVideo = - isPlayingMotionVideo != null && isPlayingMotionVideo!.value; + final showMotionVideo = useState(false); // When a video is opened through the timeline, `isCurrent` will immediately be true. // When swiping from video A to video B, `isCurrent` will initially be true for video A and false for video B. @@ -65,6 +58,25 @@ class NativeVideoViewerPage extends HookConsumerWidget { final log = Logger('NativeVideoViewerPage'); + ref.listen(isPlayingMotionVideoProvider, (_, value) async { + final videoController = controller.value; + if (!asset.isMotionPhoto || videoController == null || !context.mounted) { + return; + } + + showMotionVideo.value = value; + try { + if (value) { + await videoController.seekTo(0); + await videoController.play(); + } else { + await videoController.pause(); + } + } catch (error) { + log.severe('Error toggling motion video: $error'); + } + }); + Future createSource() async { if (!context.mounted) { return null; @@ -227,9 +239,7 @@ class NativeVideoViewerPage extends HookConsumerWidget { ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback; try { - if (asset.isVideo || - isPlayingMotionVideo == null || - isPlayingMotionVideo!.value) { + if (asset.isVideo || showMotionVideo.value) { await videoController.play(); } await videoController.setVolume(0.9); @@ -307,24 +317,6 @@ class NativeVideoViewerPage extends HookConsumerWidget { } } - void onToggleMotionVideo() async { - final videoController = controller.value; - if (videoController == null || !context.mounted) { - return; - } - - try { - if (isPlayingMotionVideo!.value) { - await videoController.seekTo(0); - await videoController.play(); - } else { - await videoController.pause(); - } - } catch (error) { - log.severe('Error toggling motion video: $error'); - } - } - void removeListeners(NativeVideoPlayerController controller) { controller.onPlaybackPositionChanged .removeListener(onPlaybackPositionChanged); @@ -397,16 +389,8 @@ class NativeVideoViewerPage extends HookConsumerWidget { () => isVisible.value = true, ); - if (isPlayingMotionVideo != null) { - isPlayingMotionVideo!.addListener(onToggleMotionVideo); - } - return () { timer?.cancel(); - if (isPlayingMotionVideo != null) { - isPlayingMotionVideo!.removeListener(onToggleMotionVideo); - } - final playerController = controller.value; if (playerController == null) { return; @@ -430,7 +414,8 @@ class NativeVideoViewerPage extends HookConsumerWidget { if (aspectRatio.value != null) Visibility.maintain( key: ValueKey(asset), - visible: (asset.isVideo || showMotionVideo) && isVisible.value, + visible: + (asset.isVideo || showMotionVideo.value) && isVisible.value, child: Center( key: ValueKey(asset), child: AspectRatio( diff --git a/mobile/lib/providers/asset_viewer/is_motion_video_playing.provider.dart b/mobile/lib/providers/asset_viewer/is_motion_video_playing.provider.dart new file mode 100644 index 0000000000..4af061f954 --- /dev/null +++ b/mobile/lib/providers/asset_viewer/is_motion_video_playing.provider.dart @@ -0,0 +1,23 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +/// Whether to display the video part of a motion photo +final isPlayingMotionVideoProvider = + StateNotifierProvider((ref) { + return IsPlayingMotionVideo(ref); +}); + +class IsPlayingMotionVideo extends StateNotifier { + IsPlayingMotionVideo(this.ref) : super(false); + + final Ref ref; + + bool get playing => state; + + set playing(bool value) { + state = value; + } + + void toggle() { + state = !state; + } +} diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index de8d041ed1..6f9e8cb396 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -1086,7 +1086,6 @@ class NativeVideoViewerRoute extends PageRouteInfo { Key? key, required Asset asset, required Widget placeholder, - ValueNotifier? isPlayingMotionVideo, bool showControls = true, List? children, }) : super( @@ -1095,7 +1094,6 @@ class NativeVideoViewerRoute extends PageRouteInfo { key: key, asset: asset, placeholder: placeholder, - isPlayingMotionVideo: isPlayingMotionVideo, showControls: showControls, ), initialChildren: children, @@ -1111,7 +1109,6 @@ class NativeVideoViewerRoute extends PageRouteInfo { key: args.key, asset: args.asset, image: args.placeholder, - isPlayingMotionVideo: args.isPlayingMotionVideo, showControls: args.showControls, ); }, @@ -1123,7 +1120,6 @@ class NativeVideoViewerRouteArgs { this.key, required this.asset, required this.placeholder, - this.isPlayingMotionVideo, this.showControls = true, }); @@ -1133,8 +1129,6 @@ class NativeVideoViewerRouteArgs { final Widget placeholder; - final ValueNotifier? isPlayingMotionVideo; - final bool showControls; @override diff --git a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart index 30cc709452..f7e2158ea9 100644 --- a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart +++ b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart @@ -21,13 +21,8 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart'; class GalleryAppBar extends ConsumerWidget { final void Function() showInfo; - final ValueNotifier isPlayingMotionVideo; - const GalleryAppBar({ - super.key, - required this.showInfo, - required this.isPlayingMotionVideo, - }); + const GalleryAppBar({super.key, required this.showInfo}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -109,15 +104,12 @@ class GalleryAppBar extends ConsumerWidget { child: TopControlAppBar( isOwner: isOwner, isPartner: isPartner, - isPlayingMotionVideo: isPlayingMotionVideo.value, asset: asset, onMoreInfoPressed: showInfo, onFavorite: toggleFavorite, onRestorePressed: () => handleRestore(asset), onUploadPressed: asset.isLocal ? () => handleUpload(asset) : null, onDownloadPressed: asset.isLocal ? null : handleDownloadAsset, - onToggleMotionVideo: () => - isPlayingMotionVideo.value = !isPlayingMotionVideo.value, onAddToAlbumPressed: () => addToAlbum(asset), onActivitiesPressed: handleActivities, ), diff --git a/mobile/lib/widgets/asset_viewer/motion_photo_button.dart b/mobile/lib/widgets/asset_viewer/motion_photo_button.dart new file mode 100644 index 0000000000..e4dd355554 --- /dev/null +++ b/mobile/lib/widgets/asset_viewer/motion_photo_button.dart @@ -0,0 +1,22 @@ +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/is_motion_video_playing.provider.dart'; + +class MotionPhotoButton extends ConsumerWidget { + const MotionPhotoButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isPlaying = ref.watch(isPlayingMotionVideoProvider); + + return IconButton( + onPressed: () { + ref.read(isPlayingMotionVideoProvider.notifier).toggle(); + }, + icon: isPlaying + ? const Icon(Icons.motion_photos_pause_outlined, color: grey200) + : const Icon(Icons.play_circle_outline_rounded, color: grey200), + ); + } +} diff --git a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart index 984b61f50c..2bdbb72ec0 100644 --- a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart +++ b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/providers/activity_statistics.provider.dart'; import 'package:immich_mobile/providers/album/current_album.provider.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; +import 'package:immich_mobile/widgets/asset_viewer/motion_photo_button.dart'; class TopControlAppBar extends HookConsumerWidget { const TopControlAppBar({ @@ -14,8 +15,6 @@ class TopControlAppBar extends HookConsumerWidget { required this.onDownloadPressed, required this.onAddToAlbumPressed, required this.onRestorePressed, - required this.onToggleMotionVideo, - required this.isPlayingMotionVideo, required this.onFavorite, required this.onUploadPressed, required this.isOwner, @@ -27,12 +26,10 @@ class TopControlAppBar extends HookConsumerWidget { final Function onMoreInfoPressed; final VoidCallback? onUploadPressed; final VoidCallback? onDownloadPressed; - final VoidCallback onToggleMotionVideo; final VoidCallback onAddToAlbumPressed; final VoidCallback onRestorePressed; final VoidCallback onActivitiesPressed; final Function(Asset) onFavorite; - final bool isPlayingMotionVideo; final bool isOwner; final bool isPartner; @@ -57,23 +54,6 @@ class TopControlAppBar extends HookConsumerWidget { ); } - Widget buildLivePhotoButton() { - return IconButton( - onPressed: () { - onToggleMotionVideo(); - }, - icon: isPlayingMotionVideo - ? Icon( - Icons.motion_photos_pause_outlined, - color: Colors.grey[200], - ) - : Icon( - Icons.play_circle_outline_rounded, - color: Colors.grey[200], - ), - ); - } - Widget buildMoreInfoButton() { return IconButton( onPressed: () { @@ -175,13 +155,11 @@ class TopControlAppBar extends HookConsumerWidget { foregroundColor: Colors.grey[100], backgroundColor: Colors.transparent, leading: buildBackButton(), - actionsIconTheme: const IconThemeData( - size: iconSize, - ), + actionsIconTheme: const IconThemeData(size: iconSize), shape: const Border(), actions: [ if (asset.isRemote && isOwner) buildFavoriteButton(a), - if (asset.livePhotoVideoId != null) buildLivePhotoButton(), + if (asset.livePhotoVideoId != null) const MotionPhotoButton(), if (asset.isLocal && !asset.isRemote) buildUploadButton(), if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(), if (asset.isRemote && (isOwner || isPartner) && !asset.isTrashed)