mirror of
https://github.com/immich-app/immich.git
synced 2025-01-28 00:59:18 -05:00
refactor(mobile): Use hooks to manage Chewie controller for video (#7008)
* video loading delayed * Chewie controller implemented in hook * fixing look and feel * Finalizing delay and animations * Fixes issue with immersive mode showing immediately in videos * format fix * Fixes bug where video controls would hide immediately after showing while playing and reverts hide controls timer to 5 seconds * Fixed rebase issues --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
b3b6426695
commit
3cd3411c1f
8 changed files with 313 additions and 238 deletions
|
@ -0,0 +1,179 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:chewie/chewie.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/asset.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/store.dart';
|
||||||
|
import 'package:video_player/video_player.dart';
|
||||||
|
import 'package:immich_mobile/shared/models/store.dart' as store;
|
||||||
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
|
|
||||||
|
/// Provides the initialized video player controller
|
||||||
|
/// If the asset is local, use the local file
|
||||||
|
/// Otherwise, use a video player with a URL
|
||||||
|
ChewieController? useChewieController(
|
||||||
|
Asset asset, {
|
||||||
|
EdgeInsets controlsSafeAreaMinimum = const EdgeInsets.only(
|
||||||
|
bottom: 100,
|
||||||
|
),
|
||||||
|
bool showOptions = true,
|
||||||
|
bool showControlsOnInitialize = false,
|
||||||
|
bool autoPlay = true,
|
||||||
|
bool autoInitialize = true,
|
||||||
|
bool allowFullScreen = false,
|
||||||
|
bool allowedScreenSleep = false,
|
||||||
|
bool showControls = true,
|
||||||
|
Widget? customControls,
|
||||||
|
Widget? placeholder,
|
||||||
|
Duration hideControlsTimer = const Duration(seconds: 1),
|
||||||
|
VoidCallback? onPlaying,
|
||||||
|
VoidCallback? onPaused,
|
||||||
|
VoidCallback? onVideoEnded,
|
||||||
|
}) {
|
||||||
|
return use(
|
||||||
|
_ChewieControllerHook(
|
||||||
|
asset: asset,
|
||||||
|
placeholder: placeholder,
|
||||||
|
showOptions: showOptions,
|
||||||
|
controlsSafeAreaMinimum: controlsSafeAreaMinimum,
|
||||||
|
autoPlay: autoPlay,
|
||||||
|
allowFullScreen: allowFullScreen,
|
||||||
|
customControls: customControls,
|
||||||
|
hideControlsTimer: hideControlsTimer,
|
||||||
|
showControlsOnInitialize: showControlsOnInitialize,
|
||||||
|
showControls: showControls,
|
||||||
|
autoInitialize: autoInitialize,
|
||||||
|
allowedScreenSleep: allowedScreenSleep,
|
||||||
|
onPlaying: onPlaying,
|
||||||
|
onPaused: onPaused,
|
||||||
|
onVideoEnded: onVideoEnded,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChewieControllerHook extends Hook<ChewieController?> {
|
||||||
|
final Asset asset;
|
||||||
|
final EdgeInsets controlsSafeAreaMinimum;
|
||||||
|
final bool showOptions;
|
||||||
|
final bool showControlsOnInitialize;
|
||||||
|
final bool autoPlay;
|
||||||
|
final bool autoInitialize;
|
||||||
|
final bool allowFullScreen;
|
||||||
|
final bool allowedScreenSleep;
|
||||||
|
final bool showControls;
|
||||||
|
final Widget? customControls;
|
||||||
|
final Widget? placeholder;
|
||||||
|
final Duration hideControlsTimer;
|
||||||
|
final VoidCallback? onPlaying;
|
||||||
|
final VoidCallback? onPaused;
|
||||||
|
final VoidCallback? onVideoEnded;
|
||||||
|
|
||||||
|
const _ChewieControllerHook({
|
||||||
|
required this.asset,
|
||||||
|
this.controlsSafeAreaMinimum = const EdgeInsets.only(
|
||||||
|
bottom: 100,
|
||||||
|
),
|
||||||
|
this.showOptions = true,
|
||||||
|
this.showControlsOnInitialize = false,
|
||||||
|
this.autoPlay = true,
|
||||||
|
this.autoInitialize = true,
|
||||||
|
this.allowFullScreen = false,
|
||||||
|
this.allowedScreenSleep = false,
|
||||||
|
this.showControls = true,
|
||||||
|
this.customControls,
|
||||||
|
this.placeholder,
|
||||||
|
this.hideControlsTimer = const Duration(seconds: 3),
|
||||||
|
this.onPlaying,
|
||||||
|
this.onPaused,
|
||||||
|
this.onVideoEnded,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
createState() => _ChewieControllerHookState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChewieControllerHookState
|
||||||
|
extends HookState<ChewieController?, _ChewieControllerHook> {
|
||||||
|
ChewieController? chewieController;
|
||||||
|
VideoPlayerController? videoPlayerController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initHook() async {
|
||||||
|
super.initHook();
|
||||||
|
unawaited(_initialize());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
chewieController?.dispose();
|
||||||
|
videoPlayerController?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ChewieController? build(BuildContext context) {
|
||||||
|
return chewieController;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initializes the chewie controller and video player controller
|
||||||
|
Future<void> _initialize() async {
|
||||||
|
if (hook.asset.isLocal && hook.asset.livePhotoVideoId == null) {
|
||||||
|
// Use a local file for the video player controller
|
||||||
|
final file = await hook.asset.local!.file;
|
||||||
|
if (file == null) {
|
||||||
|
throw Exception('No file found for the video');
|
||||||
|
}
|
||||||
|
videoPlayerController = VideoPlayerController.file(file);
|
||||||
|
} else {
|
||||||
|
// Use a network URL for the video player controller
|
||||||
|
final serverEndpoint = store.Store.get(store.StoreKey.serverEndpoint);
|
||||||
|
final String videoUrl = hook.asset.livePhotoVideoId != null
|
||||||
|
? '$serverEndpoint/asset/file/${hook.asset.livePhotoVideoId}'
|
||||||
|
: '$serverEndpoint/asset/file/${hook.asset.remoteId}';
|
||||||
|
|
||||||
|
final url = Uri.parse(videoUrl);
|
||||||
|
final accessToken = store.Store.get(StoreKey.accessToken);
|
||||||
|
|
||||||
|
videoPlayerController = VideoPlayerController.networkUrl(
|
||||||
|
url,
|
||||||
|
httpHeaders: {"x-immich-user-token": accessToken},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
videoPlayerController!.addListener(() {
|
||||||
|
final value = videoPlayerController!.value;
|
||||||
|
if (value.isPlaying) {
|
||||||
|
WakelockPlus.enable();
|
||||||
|
hook.onPlaying?.call();
|
||||||
|
} else if (!value.isPlaying) {
|
||||||
|
WakelockPlus.disable();
|
||||||
|
hook.onPaused?.call();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.position == value.duration) {
|
||||||
|
WakelockPlus.disable();
|
||||||
|
hook.onVideoEnded?.call();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await videoPlayerController!.initialize();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
chewieController = ChewieController(
|
||||||
|
videoPlayerController: videoPlayerController!,
|
||||||
|
controlsSafeAreaMinimum: hook.controlsSafeAreaMinimum,
|
||||||
|
showOptions: hook.showOptions,
|
||||||
|
showControlsOnInitialize: hook.showControlsOnInitialize,
|
||||||
|
autoPlay: hook.autoPlay,
|
||||||
|
autoInitialize: hook.autoInitialize,
|
||||||
|
allowFullScreen: hook.allowFullScreen,
|
||||||
|
allowedScreenSleep: hook.allowedScreenSleep,
|
||||||
|
showControls: hook.showControls,
|
||||||
|
customControls: hook.customControls,
|
||||||
|
placeholder: hook.placeholder,
|
||||||
|
hideControlsTimer: hook.hideControlsTimer,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provi
|
||||||
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_value_provider.dart';
|
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_value_provider.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/center_play_button.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/center_play_button.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/delayed_loading_indicator.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
class VideoPlayerControls extends ConsumerStatefulWidget {
|
class VideoPlayerControls extends ConsumerStatefulWidget {
|
||||||
|
@ -66,7 +66,9 @@ class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
|
||||||
children: [
|
children: [
|
||||||
if (_displayBufferingIndicator)
|
if (_displayBufferingIndicator)
|
||||||
const Center(
|
const Center(
|
||||||
child: ImmichLoadingIndicator(),
|
child: DelayedLoadingIndicator(
|
||||||
|
fadeInDuration: Duration(milliseconds: 400),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
_buildHitArea(),
|
_buildHitArea(),
|
||||||
|
@ -79,6 +81,7 @@ class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_dispose();
|
_dispose();
|
||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,6 +95,7 @@ class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
|
||||||
final oldController = _chewieController;
|
final oldController = _chewieController;
|
||||||
_chewieController = ChewieController.of(context);
|
_chewieController = ChewieController.of(context);
|
||||||
controller = chewieController.videoPlayerController;
|
controller = chewieController.videoPlayerController;
|
||||||
|
_latestValue = controller.value;
|
||||||
|
|
||||||
if (oldController != chewieController) {
|
if (oldController != chewieController) {
|
||||||
_dispose();
|
_dispose();
|
||||||
|
@ -106,12 +110,10 @@ class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (_latestValue.isPlaying) {
|
if (!_latestValue.isPlaying) {
|
||||||
ref.read(showControlsProvider.notifier).show = false;
|
|
||||||
} else {
|
|
||||||
_playPause();
|
_playPause();
|
||||||
ref.read(showControlsProvider.notifier).show = false;
|
|
||||||
}
|
}
|
||||||
|
ref.read(showControlsProvider.notifier).show = false;
|
||||||
},
|
},
|
||||||
child: CenterPlayButton(
|
child: CenterPlayButton(
|
||||||
backgroundColor: Colors.black54,
|
backgroundColor: Colors.black54,
|
||||||
|
@ -131,10 +133,11 @@ class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initialize() async {
|
Future<void> _initialize() async {
|
||||||
|
ref.read(showControlsProvider.notifier).show = false;
|
||||||
_mute(ref.read(videoPlayerControlsProvider.select((value) => value.mute)));
|
_mute(ref.read(videoPlayerControlsProvider.select((value) => value.mute)));
|
||||||
|
|
||||||
controller.addListener(_updateState);
|
|
||||||
_latestValue = controller.value;
|
_latestValue = controller.value;
|
||||||
|
controller.addListener(_updateState);
|
||||||
|
|
||||||
if (controller.value.isPlaying || chewieController.autoPlay) {
|
if (controller.value.isPlaying || chewieController.autoPlay) {
|
||||||
_startHideTimer();
|
_startHideTimer();
|
||||||
|
@ -167,9 +170,8 @@ class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
|
||||||
}
|
}
|
||||||
|
|
||||||
void _startHideTimer() {
|
void _startHideTimer() {
|
||||||
final hideControlsTimer = chewieController.hideControlsTimer.isNegative
|
final hideControlsTimer = chewieController.hideControlsTimer;
|
||||||
? ChewieController.defaultHideControlsTimer
|
_hideTimer?.cancel();
|
||||||
: chewieController.hideControlsTimer;
|
|
||||||
_hideTimer = Timer(hideControlsTimer, () {
|
_hideTimer = Timer(hideControlsTimer, () {
|
||||||
ref.read(showControlsProvider.notifier).show = false;
|
ref.read(showControlsProvider.notifier).show = false;
|
||||||
});
|
});
|
||||||
|
|
|
@ -704,6 +704,18 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
if (ref.read(showControlsProvider)) {
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
|
} else {
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
ref.listen(showControlsProvider, (_, show) {
|
ref.listen(showControlsProvider, (_, show) {
|
||||||
if (show) {
|
if (show) {
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
|
@ -794,7 +806,9 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||||
minScale: 1.0,
|
minScale: 1.0,
|
||||||
basePosition: Alignment.center,
|
basePosition: Alignment.center,
|
||||||
child: VideoViewerPage(
|
child: VideoViewerPage(
|
||||||
onPlaying: () => isPlayingVideo.value = true,
|
onPlaying: () {
|
||||||
|
isPlayingVideo.value = true;
|
||||||
|
},
|
||||||
onPaused: () =>
|
onPaused: () =>
|
||||||
WidgetsBinding.instance.addPostFrameCallback(
|
WidgetsBinding.instance.addPostFrameCallback(
|
||||||
(_) => isPlayingVideo.value = false,
|
(_) => isPlayingVideo.value = false,
|
||||||
|
|
|
@ -1,23 +1,15 @@
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:chewie/chewie.dart';
|
import 'package:chewie/chewie.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
import 'package:immich_mobile/modules/asset_viewer/hooks/chewiew_controller_hook.dart';
|
||||||
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
|
|
||||||
import 'package:immich_mobile/modules/asset_viewer/ui/video_player_controls.dart';
|
import 'package:immich_mobile/modules/asset_viewer/ui/video_player_controls.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/ui/delayed_loading_indicator.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
|
||||||
import 'package:video_player/video_player.dart';
|
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
// ignore: must_be_immutable
|
// ignore: must_be_immutable
|
||||||
class VideoViewerPage extends HookConsumerWidget {
|
class VideoViewerPage extends HookWidget {
|
||||||
final Asset asset;
|
final Asset asset;
|
||||||
final bool isMotionVideo;
|
final bool isMotionVideo;
|
||||||
final Widget? placeholder;
|
final Widget? placeholder;
|
||||||
|
@ -42,211 +34,49 @@ class VideoViewerPage extends HookConsumerWidget {
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context) {
|
||||||
if (asset.isLocal && asset.livePhotoVideoId == null) {
|
final controller = useChewieController(
|
||||||
final AsyncValue<File> videoFile = ref.watch(_fileFamily(asset.local!));
|
asset,
|
||||||
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(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final downloadAssetStatus =
|
|
||||||
ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
|
||||||
final String videoUrl = isMotionVideo
|
|
||||||
? '${Store.get(StoreKey.serverEndpoint)}/asset/file/${asset.livePhotoVideoId}'
|
|
||||||
: '${Store.get(StoreKey.serverEndpoint)}/asset/file/${asset.remoteId}';
|
|
||||||
|
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
VideoPlayer(
|
|
||||||
url: videoUrl,
|
|
||||||
accessToken: Store.get(StoreKey.accessToken),
|
|
||||||
isMotionVideo: isMotionVideo,
|
|
||||||
onVideoEnded: onVideoEnded,
|
|
||||||
onPaused: onPaused,
|
|
||||||
onPlaying: onPlaying,
|
|
||||||
placeholder: placeholder,
|
|
||||||
hideControlsTimer: hideControlsTimer,
|
|
||||||
showControls: showControls,
|
|
||||||
showDownloadingIndicator: showDownloadingIndicator,
|
|
||||||
),
|
|
||||||
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(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final _fileFamily =
|
|
||||||
FutureProvider.family<File, AssetEntity>((ref, entity) async {
|
|
||||||
final file = await entity.file;
|
|
||||||
if (file == null) {
|
|
||||||
throw Exception();
|
|
||||||
}
|
|
||||||
return file;
|
|
||||||
});
|
|
||||||
|
|
||||||
class VideoPlayer extends StatefulWidget {
|
|
||||||
final String? url;
|
|
||||||
final String? accessToken;
|
|
||||||
final File? file;
|
|
||||||
final bool isMotionVideo;
|
|
||||||
final VoidCallback? onVideoEnded;
|
|
||||||
final Duration hideControlsTimer;
|
|
||||||
final bool showControls;
|
|
||||||
|
|
||||||
final Function()? onPlaying;
|
|
||||||
final Function()? onPaused;
|
|
||||||
|
|
||||||
/// The placeholder to show while the video is loading
|
|
||||||
/// usually, a thumbnail of the video
|
|
||||||
final Widget? placeholder;
|
|
||||||
|
|
||||||
final bool showDownloadingIndicator;
|
|
||||||
|
|
||||||
const VideoPlayer({
|
|
||||||
super.key,
|
|
||||||
this.url,
|
|
||||||
this.accessToken,
|
|
||||||
this.file,
|
|
||||||
this.onVideoEnded,
|
|
||||||
required this.isMotionVideo,
|
|
||||||
this.onPlaying,
|
|
||||||
this.onPaused,
|
|
||||||
this.placeholder,
|
|
||||||
this.hideControlsTimer = const Duration(
|
|
||||||
seconds: 5,
|
|
||||||
),
|
|
||||||
this.showControls = true,
|
|
||||||
this.showDownloadingIndicator = true,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<VideoPlayer> createState() => _VideoPlayerState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _VideoPlayerState extends State<VideoPlayer> {
|
|
||||||
late VideoPlayerController videoPlayerController;
|
|
||||||
ChewieController? chewieController;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
initializePlayer();
|
|
||||||
|
|
||||||
videoPlayerController.addListener(() {
|
|
||||||
if (videoPlayerController.value.isInitialized) {
|
|
||||||
if (videoPlayerController.value.isPlaying) {
|
|
||||||
WakelockPlus.enable();
|
|
||||||
widget.onPlaying?.call();
|
|
||||||
} else if (!videoPlayerController.value.isPlaying) {
|
|
||||||
WakelockPlus.disable();
|
|
||||||
widget.onPaused?.call();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (videoPlayerController.value.position ==
|
|
||||||
videoPlayerController.value.duration) {
|
|
||||||
WakelockPlus.disable();
|
|
||||||
widget.onVideoEnded?.call();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> initializePlayer() async {
|
|
||||||
try {
|
|
||||||
videoPlayerController = widget.file == null
|
|
||||||
? VideoPlayerController.networkUrl(
|
|
||||||
Uri.parse(widget.url!),
|
|
||||||
httpHeaders: {"x-immich-user-token": widget.accessToken ?? ""},
|
|
||||||
)
|
|
||||||
: VideoPlayerController.file(widget.file!);
|
|
||||||
|
|
||||||
await videoPlayerController.initialize();
|
|
||||||
_createChewieController();
|
|
||||||
setState(() {});
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint("ERROR initialize video player $e");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_createChewieController() {
|
|
||||||
chewieController = ChewieController(
|
|
||||||
controlsSafeAreaMinimum: const EdgeInsets.only(
|
controlsSafeAreaMinimum: const EdgeInsets.only(
|
||||||
bottom: 100,
|
bottom: 100,
|
||||||
),
|
),
|
||||||
showOptions: true,
|
placeholder: placeholder,
|
||||||
showControlsOnInitialize: false,
|
showControls: showControls && !isMotionVideo,
|
||||||
videoPlayerController: videoPlayerController,
|
hideControlsTimer: hideControlsTimer,
|
||||||
autoPlay: true,
|
|
||||||
autoInitialize: true,
|
|
||||||
allowFullScreen: false,
|
|
||||||
allowedScreenSleep: false,
|
|
||||||
showControls: widget.showControls && !widget.isMotionVideo,
|
|
||||||
customControls: const VideoPlayerControls(),
|
customControls: const VideoPlayerControls(),
|
||||||
hideControlsTimer: widget.hideControlsTimer,
|
onPlaying: onPlaying,
|
||||||
|
onPaused: onPaused,
|
||||||
|
onVideoEnded: onVideoEnded,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Loading
|
||||||
|
return PopScope(
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 400),
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
if (controller == null) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
if (placeholder != null) placeholder!,
|
||||||
|
const DelayedLoadingIndicator(
|
||||||
|
fadeInDuration: Duration(milliseconds: 500),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final size = MediaQuery.of(context).size;
|
||||||
|
return SizedBox(
|
||||||
|
height: size.height,
|
||||||
|
width: size.width,
|
||||||
|
child: Chewie(
|
||||||
|
controller: controller,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
super.dispose();
|
|
||||||
videoPlayerController.pause();
|
|
||||||
videoPlayerController.dispose();
|
|
||||||
chewieController?.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (chewieController?.videoPlayerController.value.isInitialized == true) {
|
|
||||||
return SizedBox(
|
|
||||||
height: context.height,
|
|
||||||
width: context.width,
|
|
||||||
child: Chewie(
|
|
||||||
controller: chewieController!,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return SizedBox(
|
|
||||||
height: context.height,
|
|
||||||
width: context.width,
|
|
||||||
child: Center(
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
if (widget.placeholder != null) widget.placeholder!,
|
|
||||||
if (widget.showDownloadingIndicator)
|
|
||||||
const Center(
|
|
||||||
child: ImmichLoadingIndicator(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1393,7 +1393,7 @@ class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
|
||||||
void Function()? onPaused,
|
void Function()? onPaused,
|
||||||
Widget? placeholder,
|
Widget? placeholder,
|
||||||
bool showControls = true,
|
bool showControls = true,
|
||||||
Duration hideControlsTimer = const Duration(seconds: 5),
|
Duration hideControlsTimer = const Duration(milliseconds: 1500),
|
||||||
bool showDownloadingIndicator = true,
|
bool showDownloadingIndicator = true,
|
||||||
List<PageRouteInfo>? children,
|
List<PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
|
@ -1429,7 +1429,7 @@ class VideoViewerRouteArgs {
|
||||||
this.onPaused,
|
this.onPaused,
|
||||||
this.placeholder,
|
this.placeholder,
|
||||||
this.showControls = true,
|
this.showControls = true,
|
||||||
this.hideControlsTimer = const Duration(seconds: 5),
|
this.hideControlsTimer = const Duration(milliseconds: 1500),
|
||||||
this.showDownloadingIndicator = true,
|
this.showDownloadingIndicator = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -171,6 +171,11 @@ class Asset {
|
||||||
|
|
||||||
int? stackCount;
|
int? stackCount;
|
||||||
|
|
||||||
|
/// Aspect ratio of the asset
|
||||||
|
@ignore
|
||||||
|
double? get aspectRatio =>
|
||||||
|
width == null || height == null ? 0 : width! / height!;
|
||||||
|
|
||||||
/// `true` if this [Asset] is present on the device
|
/// `true` if this [Asset] is present on the device
|
||||||
@ignore
|
@ignore
|
||||||
bool get isLocal => localId != null;
|
bool get isLocal => localId != null;
|
||||||
|
|
40
mobile/lib/shared/ui/delayed_loading_indicator.dart
Normal file
40
mobile/lib/shared/ui/delayed_loading_indicator.dart
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
|
|
||||||
|
class DelayedLoadingIndicator extends StatelessWidget {
|
||||||
|
/// The delay to avoid showing the loading indicator
|
||||||
|
final Duration delay;
|
||||||
|
|
||||||
|
/// Defaults to using the [ImmichLoadingIndicator]
|
||||||
|
final Widget? child;
|
||||||
|
|
||||||
|
/// An optional fade in duration to animate the loading
|
||||||
|
final Duration? fadeInDuration;
|
||||||
|
|
||||||
|
const DelayedLoadingIndicator({
|
||||||
|
super.key,
|
||||||
|
this.delay = const Duration(seconds: 3),
|
||||||
|
this.child,
|
||||||
|
this.fadeInDuration,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedSwitcher(
|
||||||
|
duration: fadeInDuration ?? Duration.zero,
|
||||||
|
child: FutureBuilder(
|
||||||
|
future: Future.delayed(delay),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.done) {
|
||||||
|
return child ??
|
||||||
|
const ImmichLoadingIndicator(
|
||||||
|
key: ValueKey('loading'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(key: const ValueKey('hiding'));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/delayed_loading_indicator.dart';
|
||||||
|
|
||||||
final _loadingEntry = OverlayEntry(
|
final _loadingEntry = OverlayEntry(
|
||||||
builder: (context) => SizedBox.square(
|
builder: (context) => SizedBox.square(
|
||||||
|
@ -9,7 +9,12 @@ final _loadingEntry = OverlayEntry(
|
||||||
child: DecoratedBox(
|
child: DecoratedBox(
|
||||||
decoration:
|
decoration:
|
||||||
BoxDecoration(color: context.colorScheme.surface.withAlpha(200)),
|
BoxDecoration(color: context.colorScheme.surface.withAlpha(200)),
|
||||||
child: const Center(child: ImmichLoadingIndicator()),
|
child: const Center(
|
||||||
|
child: DelayedLoadingIndicator(
|
||||||
|
delay: Duration(seconds: 1),
|
||||||
|
fadeInDuration: Duration(milliseconds: 400),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -27,19 +32,19 @@ class _LoadingOverlay extends Hook<ValueNotifier<bool>> {
|
||||||
|
|
||||||
class _LoadingOverlayState
|
class _LoadingOverlayState
|
||||||
extends HookState<ValueNotifier<bool>, _LoadingOverlay> {
|
extends HookState<ValueNotifier<bool>, _LoadingOverlay> {
|
||||||
late final _isProcessing = ValueNotifier(false)..addListener(_listener);
|
late final _isLoading = ValueNotifier(false)..addListener(_listener);
|
||||||
OverlayEntry? overlayEntry;
|
OverlayEntry? _loadingOverlay;
|
||||||
|
|
||||||
void _listener() {
|
void _listener() {
|
||||||
setState(() {
|
setState(() {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (_isProcessing.value) {
|
if (_isLoading.value) {
|
||||||
overlayEntry?.remove();
|
_loadingOverlay?.remove();
|
||||||
overlayEntry = _loadingEntry;
|
_loadingOverlay = _loadingEntry;
|
||||||
Overlay.of(context).insert(_loadingEntry);
|
Overlay.of(context).insert(_loadingEntry);
|
||||||
} else {
|
} else {
|
||||||
overlayEntry?.remove();
|
_loadingOverlay?.remove();
|
||||||
overlayEntry = null;
|
_loadingOverlay = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -47,17 +52,17 @@ class _LoadingOverlayState
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ValueNotifier<bool> build(BuildContext context) {
|
ValueNotifier<bool> build(BuildContext context) {
|
||||||
return _isProcessing;
|
return _isLoading;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_isProcessing.dispose();
|
_isLoading.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Object? get debugValue => _isProcessing.value;
|
Object? get debugValue => _isLoading.value;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get debugLabel => 'useProcessingOverlay<>';
|
String get debugLabel => 'useProcessingOverlay<>';
|
||||||
|
|
Loading…
Add table
Reference in a new issue