From 99fce9a7bd754ba1b263abb7e6d98d8091a2652a Mon Sep 17 00:00:00 2001 From: Marty Fuhry Date: Sun, 4 Feb 2024 16:15:19 -0500 Subject: [PATCH 1/2] Videos play in memories now Remove auto video advance --- .../asset_viewer/views/video_viewer_page.dart | 80 ++++++++++++------- .../lib/modules/memories/ui/memory_card.dart | 57 ++++++++----- .../modules/memories/views/memory_page.dart | 26 ++++-- 3 files changed, 108 insertions(+), 55 deletions(-) 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 8257f28578176..72aa397f671e4 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? accessToken; 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.accessToken, 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, ); } @@ -216,9 +239,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 f9231338c5d55..364a88b47c2b7 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 26104054c3c53..d6a4d2ae6fdf6 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: () { + // TODO: At the end of the video, advance to the next asset automatically. If this is a live photo, don't go to + // next asset + //if (asset.livePhotoVideoId == null) { + //toNextAsset(index); + //} + //}, + ), ), ); }, From 534fb73491aff2896b07c1d978e749168ab28122 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Wed, 7 Feb 2024 22:04:45 -0600 Subject: [PATCH 2/2] to next asset after video is done playing --- mobile/lib/modules/memories/views/memory_page.dart | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/mobile/lib/modules/memories/views/memory_page.dart b/mobile/lib/modules/memories/views/memory_page.dart index d6a4d2ae6fdf6..63f4e7df04fc1 100644 --- a/mobile/lib/modules/memories/views/memory_page.dart +++ b/mobile/lib/modules/memories/views/memory_page.dart @@ -256,13 +256,9 @@ class MemoryPage extends HookConsumerWidget { asset: asset, title: memories[mIndex].title, showTitle: index == 0, - //onVideoEnded: () { - // TODO: At the end of the video, advance to the next asset automatically. If this is a live photo, don't go to - // next asset - //if (asset.livePhotoVideoId == null) { - //toNextAsset(index); - //} - //}, + onVideoEnded: () { + toNextAsset(index); + }, ), ), );