diff --git a/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart b/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart index d8e6b704fb..80034ae464 100644 --- a/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart +++ b/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart @@ -21,40 +21,45 @@ class VideoViewerPage extends HookConsumerWidget { final Asset asset; final bool isMotionVideo; final Widget? placeholder; - final VoidCallback onVideoEnded; + final VoidCallback? onVideoEnded; final VoidCallback? onPlaying; final VoidCallback? onPaused; + final Duration hideControlsTimer; + final bool showControls; + final bool showDownloadingIndicator; const VideoViewerPage({ super.key, required this.asset, - required this.isMotionVideo, - required this.onVideoEnded, + this.isMotionVideo = false, + this.onVideoEnded, this.onPlaying, this.onPaused, this.placeholder, + this.showControls = true, + this.hideControlsTimer = const Duration(seconds: 5), + this.showDownloadingIndicator = true, }); @override Widget build(BuildContext context, WidgetRef ref) { if (asset.isLocal && asset.livePhotoVideoId == null) { final AsyncValue videoFile = ref.watch(_fileFamily(asset.local!)); - return videoFile.when( - data: (data) => VideoPlayer( - file: data, - isMotionVideo: false, - onVideoEnded: () {}, - ), - error: (error, stackTrace) => Icon( - Icons.image_not_supported_outlined, - color: context.primaryColor, - ), - loading: () => const Center( - child: SizedBox( - width: 75, - height: 75, - child: CircularProgressIndicator.adaptive(), + return AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: videoFile.when( + data: (data) => VideoPlayer( + file: data, + isMotionVideo: false, + onVideoEnded: () {}, ), + error: (error, stackTrace) => Icon( + Icons.image_not_supported_outlined, + color: context.primaryColor, + ), + loading: () => showDownloadingIndicator + ? const Center(child: ImmichLoadingIndicator()) + : Container(), ), ); } @@ -74,15 +79,24 @@ class VideoViewerPage extends HookConsumerWidget { onPaused: onPaused, onPlaying: onPlaying, placeholder: placeholder, + hideControlsTimer: hideControlsTimer, + showControls: showControls, + showDownloadingIndicator: showDownloadingIndicator, ), - if (downloadAssetStatus == DownloadAssetStatus.loading) - SizedBox( + AnimatedOpacity( + duration: const Duration(milliseconds: 400), + opacity: (downloadAssetStatus == DownloadAssetStatus.loading && + showDownloadingIndicator) + ? 1.0 + : 0.0, + child: SizedBox( height: context.height, width: context.width, child: const Center( child: ImmichLoadingIndicator(), ), ), + ), ], ); } @@ -102,7 +116,9 @@ class VideoPlayer extends StatefulWidget { final String? jwtToken; final File? file; final bool isMotionVideo; - final VoidCallback onVideoEnded; + final VoidCallback? onVideoEnded; + final Duration hideControlsTimer; + final bool showControls; final Function()? onPlaying; final Function()? onPaused; @@ -111,16 +127,23 @@ class VideoPlayer extends StatefulWidget { /// usually, a thumbnail of the video final Widget? placeholder; + final bool showDownloadingIndicator; + const VideoPlayer({ super.key, this.url, this.jwtToken, this.file, - required this.onVideoEnded, + this.onVideoEnded, required this.isMotionVideo, this.onPlaying, this.onPaused, this.placeholder, + this.hideControlsTimer = const Duration( + seconds: 5, + ), + this.showControls = true, + this.showDownloadingIndicator = true, }); @override @@ -149,7 +172,7 @@ class _VideoPlayerState extends State { if (videoPlayerController.value.position == videoPlayerController.value.duration) { WakelockPlus.disable(); - widget.onVideoEnded(); + widget.onVideoEnded?.call(); } } }); @@ -184,9 +207,9 @@ class _VideoPlayerState extends State { autoInitialize: true, allowFullScreen: false, allowedScreenSleep: false, - showControls: !widget.isMotionVideo, + showControls: widget.showControls && !widget.isMotionVideo, customControls: const VideoPlayerControls(), - hideControlsTimer: const Duration(seconds: 5), + hideControlsTimer: widget.hideControlsTimer, ); } @@ -214,9 +237,10 @@ class _VideoPlayerState extends State { child: Stack( children: [ if (widget.placeholder != null) widget.placeholder!, - const Center( - child: ImmichLoadingIndicator(), - ), + if (widget.showDownloadingIndicator) + const Center( + child: ImmichLoadingIndicator(), + ), ], ), ), diff --git a/mobile/lib/modules/memories/ui/memory_card.dart b/mobile/lib/modules/memories/ui/memory_card.dart index 9c517c738b..dde98e605e 100644 --- a/mobile/lib/modules/memories/ui/memory_card.dart +++ b/mobile/lib/modules/memories/ui/memory_card.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart'; @@ -11,15 +12,15 @@ import 'package:openapi/api.dart'; class MemoryCard extends StatelessWidget { final Asset asset; - final void Function() onTap; final String title; final bool showTitle; + final Function()? onVideoEnded; const MemoryCard({ required this.asset, - required this.onTap, required this.title, required this.showTitle, + this.onVideoEnded, super.key, }); @@ -59,24 +60,23 @@ class MemoryCard extends StatelessWidget { child: Container(color: Colors.black.withOpacity(0.2)), ), ), - GestureDetector( - onTap: onTap, - child: LayoutBuilder( - builder: (context, constraints) { - // Determine the fit using the aspect ratio - BoxFit fit = BoxFit.fitWidth; - if (asset.width != null && asset.height != null) { - final aspectRatio = asset.width! / asset.height!; - final phoneAspectRatio = - constraints.maxWidth / constraints.maxHeight; - // Look for a 25% difference in either direction - if (phoneAspectRatio * .75 < aspectRatio && - phoneAspectRatio * 1.25 > aspectRatio) { - // Cover to look nice if we have nearly the same aspect ratio - fit = BoxFit.cover; - } + LayoutBuilder( + builder: (context, constraints) { + // Determine the fit using the aspect ratio + BoxFit fit = BoxFit.fitWidth; + if (asset.width != null && asset.height != null) { + final aspectRatio = asset.height! / asset.width!; + final phoneAspectRatio = + constraints.maxWidth / constraints.maxHeight; + // Look for a 25% difference in either direction + if (phoneAspectRatio * .75 < aspectRatio && + phoneAspectRatio * 1.25 > aspectRatio) { + // Cover to look nice if we have nearly the same aspect ratio + fit = BoxFit.cover; } + } + if (asset.isImage) { return Hero( tag: 'memory-${asset.id}', child: ImmichImage( @@ -88,8 +88,25 @@ class MemoryCard extends StatelessWidget { preferredLocalAssetSize: 2048, ), ); - }, - ), + } else { + return Hero( + tag: 'memory-${asset.id}', + child: VideoViewerPage( + asset: asset, + showDownloadingIndicator: false, + placeholder: ImmichImage( + asset, + fit: fit, + type: ThumbnailFormat.JPEG, + preferredLocalAssetSize: 2048, + ), + hideControlsTimer: const Duration(seconds: 2), + onVideoEnded: onVideoEnded, + showControls: false, + ), + ); + } + }, ), if (showTitle) Positioned( diff --git a/mobile/lib/modules/memories/views/memory_page.dart b/mobile/lib/modules/memories/views/memory_page.dart index 26104054c3..c8a67ba824 100644 --- a/mobile/lib/modules/memories/views/memory_page.dart +++ b/mobile/lib/modules/memories/views/memory_page.dart @@ -245,13 +245,25 @@ class MemoryPage extends HookConsumerWidget { itemCount: memories[mIndex].assets.length, itemBuilder: (context, index) { final asset = memories[mIndex].assets[index]; - return Container( - color: Colors.black, - child: MemoryCard( - asset: asset, - onTap: () => toNextAsset(index), - title: memories[mIndex].title, - showTitle: index == 0, + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + toNextAsset(index); + }, + child: Container( + color: Colors.black, + child: MemoryCard( + asset: asset, + title: memories[mIndex].title, + showTitle: index == 0, + onVideoEnded: () { + // If this is a live photo, don't go to + // next asset + if (asset.livePhotoVideoId == null) { + toNextAsset(index); + } + }, + ), ), ); },