1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00:00

Videos play in memories now

This commit is contained in:
Marty Fuhry 2024-02-04 16:15:19 -05:00
parent b0ff859cd6
commit f14295e107
No known key found for this signature in database
GPG key ID: E2AB6392D894D900
3 changed files with 108 additions and 55 deletions

View file

@ -21,40 +21,45 @@ class VideoViewerPage extends HookConsumerWidget {
final Asset asset; final Asset asset;
final bool isMotionVideo; final bool isMotionVideo;
final Widget? placeholder; final Widget? placeholder;
final VoidCallback onVideoEnded; final VoidCallback? onVideoEnded;
final VoidCallback? onPlaying; final VoidCallback? onPlaying;
final VoidCallback? onPaused; final VoidCallback? onPaused;
final Duration hideControlsTimer;
final bool showControls;
final bool showDownloadingIndicator;
const VideoViewerPage({ const VideoViewerPage({
super.key, super.key,
required this.asset, required this.asset,
required this.isMotionVideo, this.isMotionVideo = false,
required this.onVideoEnded, this.onVideoEnded,
this.onPlaying, this.onPlaying,
this.onPaused, this.onPaused,
this.placeholder, this.placeholder,
this.showControls = true,
this.hideControlsTimer = const Duration(seconds: 5),
this.showDownloadingIndicator = true,
}); });
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
if (asset.isLocal && asset.livePhotoVideoId == null) { if (asset.isLocal && asset.livePhotoVideoId == null) {
final AsyncValue<File> videoFile = ref.watch(_fileFamily(asset.local!)); final AsyncValue<File> videoFile = ref.watch(_fileFamily(asset.local!));
return videoFile.when( return AnimatedSwitcher(
data: (data) => VideoPlayer( duration: const Duration(milliseconds: 200),
file: data, child: videoFile.when(
isMotionVideo: false, data: (data) => VideoPlayer(
onVideoEnded: () {}, file: data,
), isMotionVideo: false,
error: (error, stackTrace) => Icon( onVideoEnded: () {},
Icons.image_not_supported_outlined,
color: context.primaryColor,
),
loading: () => const Center(
child: SizedBox(
width: 75,
height: 75,
child: CircularProgressIndicator.adaptive(),
), ),
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, onPaused: onPaused,
onPlaying: onPlaying, onPlaying: onPlaying,
placeholder: placeholder, placeholder: placeholder,
hideControlsTimer: hideControlsTimer,
showControls: showControls,
showDownloadingIndicator: showDownloadingIndicator,
), ),
if (downloadAssetStatus == DownloadAssetStatus.loading) AnimatedOpacity(
SizedBox( duration: const Duration(milliseconds: 400),
opacity: (downloadAssetStatus == DownloadAssetStatus.loading &&
showDownloadingIndicator)
? 1.0
: 0.0,
child: SizedBox(
height: context.height, height: context.height,
width: context.width, width: context.width,
child: const Center( child: const Center(
child: ImmichLoadingIndicator(), child: ImmichLoadingIndicator(),
), ),
), ),
),
], ],
); );
} }
@ -102,7 +116,9 @@ class VideoPlayer extends StatefulWidget {
final String? jwtToken; final String? jwtToken;
final File? file; final File? file;
final bool isMotionVideo; final bool isMotionVideo;
final VoidCallback onVideoEnded; final VoidCallback? onVideoEnded;
final Duration hideControlsTimer;
final bool showControls;
final Function()? onPlaying; final Function()? onPlaying;
final Function()? onPaused; final Function()? onPaused;
@ -111,16 +127,23 @@ class VideoPlayer extends StatefulWidget {
/// usually, a thumbnail of the video /// usually, a thumbnail of the video
final Widget? placeholder; final Widget? placeholder;
final bool showDownloadingIndicator;
const VideoPlayer({ const VideoPlayer({
super.key, super.key,
this.url, this.url,
this.jwtToken, this.jwtToken,
this.file, this.file,
required this.onVideoEnded, this.onVideoEnded,
required this.isMotionVideo, required this.isMotionVideo,
this.onPlaying, this.onPlaying,
this.onPaused, this.onPaused,
this.placeholder, this.placeholder,
this.hideControlsTimer = const Duration(
seconds: 5,
),
this.showControls = true,
this.showDownloadingIndicator = true,
}); });
@override @override
@ -149,7 +172,7 @@ class _VideoPlayerState extends State<VideoPlayer> {
if (videoPlayerController.value.position == if (videoPlayerController.value.position ==
videoPlayerController.value.duration) { videoPlayerController.value.duration) {
WakelockPlus.disable(); WakelockPlus.disable();
widget.onVideoEnded(); widget.onVideoEnded?.call();
} }
} }
}); });
@ -184,9 +207,9 @@ class _VideoPlayerState extends State<VideoPlayer> {
autoInitialize: true, autoInitialize: true,
allowFullScreen: false, allowFullScreen: false,
allowedScreenSleep: false, allowedScreenSleep: false,
showControls: !widget.isMotionVideo, showControls: widget.showControls && !widget.isMotionVideo,
customControls: const VideoPlayerControls(), customControls: const VideoPlayerControls(),
hideControlsTimer: const Duration(seconds: 5), hideControlsTimer: widget.hideControlsTimer,
); );
} }
@ -214,9 +237,10 @@ class _VideoPlayerState extends State<VideoPlayer> {
child: Stack( child: Stack(
children: [ children: [
if (widget.placeholder != null) widget.placeholder!, if (widget.placeholder != null) widget.placeholder!,
const Center( if (widget.showDownloadingIndicator)
child: ImmichLoadingIndicator(), const Center(
), child: ImmichLoadingIndicator(),
),
], ],
), ),
), ),

View file

@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.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/asset.dart';
import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart';
@ -11,15 +12,15 @@ import 'package:openapi/api.dart';
class MemoryCard extends StatelessWidget { class MemoryCard extends StatelessWidget {
final Asset asset; final Asset asset;
final void Function() onTap;
final String title; final String title;
final bool showTitle; final bool showTitle;
final Function()? onVideoEnded;
const MemoryCard({ const MemoryCard({
required this.asset, required this.asset,
required this.onTap,
required this.title, required this.title,
required this.showTitle, required this.showTitle,
this.onVideoEnded,
super.key, super.key,
}); });
@ -59,24 +60,23 @@ class MemoryCard extends StatelessWidget {
child: Container(color: Colors.black.withOpacity(0.2)), child: Container(color: Colors.black.withOpacity(0.2)),
), ),
), ),
GestureDetector( LayoutBuilder(
onTap: onTap, builder: (context, constraints) {
child: LayoutBuilder( // Determine the fit using the aspect ratio
builder: (context, constraints) { BoxFit fit = BoxFit.fitWidth;
// Determine the fit using the aspect ratio if (asset.width != null && asset.height != null) {
BoxFit fit = BoxFit.fitWidth; final aspectRatio = asset.height! / asset.width!;
if (asset.width != null && asset.height != null) { final phoneAspectRatio =
final aspectRatio = asset.width! / asset.height!; constraints.maxWidth / constraints.maxHeight;
final phoneAspectRatio = // Look for a 25% difference in either direction
constraints.maxWidth / constraints.maxHeight; if (phoneAspectRatio * .75 < aspectRatio &&
// Look for a 25% difference in either direction phoneAspectRatio * 1.25 > aspectRatio) {
if (phoneAspectRatio * .75 < aspectRatio && // Cover to look nice if we have nearly the same aspect ratio
phoneAspectRatio * 1.25 > aspectRatio) { fit = BoxFit.cover;
// Cover to look nice if we have nearly the same aspect ratio
fit = BoxFit.cover;
}
} }
}
if (asset.isImage) {
return Hero( return Hero(
tag: 'memory-${asset.id}', tag: 'memory-${asset.id}',
child: ImmichImage( child: ImmichImage(
@ -88,8 +88,25 @@ class MemoryCard extends StatelessWidget {
preferredLocalAssetSize: 2048, 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) if (showTitle)
Positioned( Positioned(

View file

@ -245,13 +245,25 @@ class MemoryPage extends HookConsumerWidget {
itemCount: memories[mIndex].assets.length, itemCount: memories[mIndex].assets.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final asset = memories[mIndex].assets[index]; final asset = memories[mIndex].assets[index];
return Container( return GestureDetector(
color: Colors.black, behavior: HitTestBehavior.translucent,
child: MemoryCard( onTap: () {
asset: asset, toNextAsset(index);
onTap: () => toNextAsset(index), },
title: memories[mIndex].title, child: Container(
showTitle: index == 0, 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);
}
},
),
), ),
); );
}, },