refactor(mobile): Refactor video player page and gallery bottom app bar (#7625)

* Fixes double video auto initialize issue and placeholder for video controller

* WIP unravel stack index

* Refactors video player controller

format

fixing video

format

Working

format

* Fixes hide on pause

* Got hiding when tapped working

* Hides controls when video starts and fixes placeholder for memory card

Remove prints

* Fixes show controls with microtask

* fix LivePhotos not playing

* removes unused function callbacks and moves wakelock

* Update motion video

* Fixing motion photo playing

* Renames to isPlayingVideo

* Fixes playing video on change

* pause on dispose

* fixing issues with sync between controls

* Adds gallery app bar

* Switches to memoized

* Fixes pause

* Revert "Switches to memoized"

This reverts commit 234e6741de.

* uses stateful widget

* Fixes double video play by using provider and new chewie video player

wip

format

Fixes motion photos

format

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
martyfuhry
2024-03-05 22:42:22 -05:00
committed by GitHub
parent 2f53f6a62c
commit 4ef4cc8016
21 changed files with 1528 additions and 933 deletions
@@ -0,0 +1,345 @@
import 'dart:io';
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/services/asset_stack.service.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/video_controls.dart';
import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/providers/user.provider.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
class BottomGalleryBar extends ConsumerWidget {
final Asset asset;
final bool showStack;
final int stackIndex;
final int totalAssets;
final bool showVideoPlayerControls;
final PageController controller;
const BottomGalleryBar({
super.key,
required this.showStack,
required this.stackIndex,
required this.asset,
required this.controller,
required this.totalAssets,
required this.showVideoPlayerControls,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.isarId;
final stack = showStack && asset.stackChildrenCount > 0
? ref.watch(assetStackStateProvider(asset))
: <Asset>[];
final stackElements = showStack ? [asset, ...stack] : <Asset>[];
bool isParent = stackIndex == -1 || stackIndex == 0;
final navStack = AutoRouter.of(context).stackData;
final isTrashEnabled =
ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash));
final isFromTrash = isTrashEnabled &&
navStack.length > 2 &&
navStack.elementAt(navStack.length - 2).name == TrashRoute.name;
// !!!! itemsList and actionlist should always be in sync
final itemsList = [
BottomNavigationBarItem(
icon: Icon(
Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded,
),
label: 'control_bottom_app_bar_share'.tr(),
tooltip: 'control_bottom_app_bar_share'.tr(),
),
if (isOwner)
asset.isArchived
? BottomNavigationBarItem(
icon: const Icon(Icons.unarchive_rounded),
label: 'control_bottom_app_bar_unarchive'.tr(),
tooltip: 'control_bottom_app_bar_unarchive'.tr(),
)
: BottomNavigationBarItem(
icon: const Icon(Icons.archive_outlined),
label: 'control_bottom_app_bar_archive'.tr(),
tooltip: 'control_bottom_app_bar_archive'.tr(),
),
if (isOwner && stack.isNotEmpty)
BottomNavigationBarItem(
icon: const Icon(Icons.burst_mode_outlined),
label: 'control_bottom_app_bar_stack'.tr(),
tooltip: 'control_bottom_app_bar_stack'.tr(),
),
if (isOwner)
BottomNavigationBarItem(
icon: const Icon(Icons.delete_outline),
label: 'control_bottom_app_bar_delete'.tr(),
tooltip: 'control_bottom_app_bar_delete'.tr(),
),
if (!isOwner)
BottomNavigationBarItem(
icon: const Icon(Icons.download_outlined),
label: 'download'.tr(),
tooltip: 'download'.tr(),
),
];
void removeAssetFromStack() {
if (stackIndex > 0 && showStack) {
ref
.read(assetStackStateProvider(asset).notifier)
.removeChild(stackIndex - 1);
}
}
void handleDelete() async {
// Cannot delete readOnly / external assets. They are handled through library offline jobs
if (asset.isReadOnly) {
ImmichToast.show(
durationInSecond: 1,
context: context,
msg: 'asset_action_delete_err_read_only'.tr(),
gravity: ToastGravity.BOTTOM,
);
return;
}
Future<bool> onDelete(bool force) async {
final isDeleted = await ref.read(assetProvider.notifier).deleteAssets(
{asset},
force: force,
);
if (isDeleted && isParent) {
if (totalAssets == 1) {
// Handle only one asset
context.popRoute();
} else {
// Go to next page otherwise
controller.nextPage(
duration: const Duration(milliseconds: 100),
curve: Curves.fastLinearToSlowEaseIn,
);
}
}
return isDeleted;
}
// Asset is trashed
if (isTrashEnabled && !isFromTrash) {
final isDeleted = await onDelete(false);
if (isDeleted) {
// Can only trash assets stored in server. Local assets are always permanently removed for now
if (context.mounted && asset.isRemote && isParent) {
ImmichToast.show(
durationInSecond: 1,
context: context,
msg: 'Asset trashed',
gravity: ToastGravity.BOTTOM,
);
}
removeAssetFromStack();
}
return;
}
// Asset is permanently removed
showDialog(
context: context,
builder: (BuildContext _) {
return DeleteDialog(
onDelete: () async {
final isDeleted = await onDelete(true);
if (isDeleted) {
removeAssetFromStack();
}
},
);
},
);
}
void showStackActionItems() {
showModalBottomSheet<void>(
context: context,
enableDrag: false,
builder: (BuildContext ctx) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.only(top: 24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (!isParent)
ListTile(
leading: const Icon(
Icons.bookmark_border_outlined,
size: 24,
),
onTap: () async {
await ref
.read(assetStackServiceProvider)
.updateStackParent(
asset,
stackElements.elementAt(stackIndex),
);
ctx.pop();
context.popRoute();
},
title: const Text(
"viewer_stack_use_as_main_asset",
style: TextStyle(fontWeight: FontWeight.bold),
).tr(),
),
ListTile(
leading: const Icon(
Icons.copy_all_outlined,
size: 24,
),
onTap: () async {
if (isParent) {
await ref
.read(assetStackServiceProvider)
.updateStackParent(
asset,
stackElements
.elementAt(1), // Next asset as parent
);
// Remove itself from stack
await ref.read(assetStackServiceProvider).updateStack(
stackElements.elementAt(1),
childrenToRemove: [asset],
);
ctx.pop();
context.popRoute();
} else {
await ref.read(assetStackServiceProvider).updateStack(
asset,
childrenToRemove: [
stackElements.elementAt(stackIndex),
],
);
removeAssetFromStack();
ctx.pop();
}
},
title: const Text(
"viewer_remove_from_stack",
style: TextStyle(fontWeight: FontWeight.bold),
).tr(),
),
ListTile(
leading: const Icon(
Icons.filter_none_outlined,
size: 18,
),
onTap: () async {
await ref.read(assetStackServiceProvider).updateStack(
asset,
childrenToRemove: stack,
);
ctx.pop();
context.popRoute();
},
title: const Text(
"viewer_unstack",
style: TextStyle(fontWeight: FontWeight.bold),
).tr(),
),
],
),
),
);
},
);
}
shareAsset() {
if (asset.isOffline) {
ImmichToast.show(
durationInSecond: 1,
context: context,
msg: 'asset_action_share_err_offline'.tr(),
gravity: ToastGravity.BOTTOM,
);
return;
}
ref.read(imageViewerStateProvider.notifier).shareAsset(asset, context);
}
handleArchive() {
ref.read(assetProvider.notifier).toggleArchive([asset]);
if (isParent) {
context.popRoute();
return;
}
removeAssetFromStack();
}
handleDownload() {
if (asset.isLocal) {
return;
}
if (asset.isOffline) {
ImmichToast.show(
durationInSecond: 1,
context: context,
msg: 'asset_action_share_err_offline'.tr(),
gravity: ToastGravity.BOTTOM,
);
return;
}
ref.read(imageViewerStateProvider.notifier).downloadAsset(
asset,
context,
);
}
List<Function(int)> actionslist = [
(_) => shareAsset(),
if (isOwner) (_) => handleArchive(),
if (isOwner && stack.isNotEmpty) (_) => showStackActionItems(),
if (isOwner) (_) => handleDelete(),
if (!isOwner) (_) => handleDownload(),
];
return IgnorePointer(
ignoring: !ref.watch(showControlsProvider),
child: AnimatedOpacity(
duration: const Duration(milliseconds: 100),
opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0,
child: Column(
children: [
Visibility(
visible: showVideoPlayerControls,
child: const VideoControls(),
),
BottomNavigationBar(
backgroundColor: Colors.black.withOpacity(0.4),
unselectedIconTheme: const IconThemeData(color: Colors.white),
selectedIconTheme: const IconThemeData(color: Colors.white),
unselectedLabelStyle: const TextStyle(color: Colors.black),
selectedLabelStyle: const TextStyle(color: Colors.black),
showSelectedLabels: false,
showUnselectedLabels: false,
items: itemsList,
onTap: (index) {
if (index < actionslist.length) {
actionslist[index].call(index);
}
},
),
],
),
),
);
}
}
@@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/show_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/ui/center_play_button.dart';
import 'package:immich_mobile/shared/ui/delayed_loading_indicator.dart';
import 'package:immich_mobile/shared/ui/hooks/timer_hook.dart';
class CustomVideoPlayerControls extends HookConsumerWidget {
final Duration hideTimerDuration;
const CustomVideoPlayerControls({
super.key,
this.hideTimerDuration = const Duration(seconds: 3),
});
@override
Widget build(BuildContext context, WidgetRef ref) {
// A timer to hide the controls
final hideTimer = useTimer(
hideTimerDuration,
() {
final state = ref.read(videoPlaybackValueProvider).state;
// Do not hide on paused
if (state != VideoPlaybackState.paused) {
ref.read(showControlsProvider.notifier).show = false;
}
},
);
final showBuffering = useState(false);
final VideoPlaybackState state =
ref.watch(videoPlaybackValueProvider).state;
/// Shows the controls and starts the timer to hide them
void showControlsAndStartHideTimer() {
hideTimer.reset();
ref.read(showControlsProvider.notifier).show = true;
}
// When we mute, show the controls
ref.listen(videoPlayerControlsProvider.select((v) => v.mute),
(previous, next) {
showControlsAndStartHideTimer();
});
// When we change position, show or hide timer
ref.listen(videoPlayerControlsProvider.select((v) => v.position),
(previous, next) {
showControlsAndStartHideTimer();
});
ref.listen(videoPlaybackValueProvider.select((value) => value.state),
(_, state) {
// Show buffering
showBuffering.value = state == VideoPlaybackState.buffering;
});
/// Toggles between playing and pausing depending on the state of the video
void togglePlay() {
showControlsAndStartHideTimer();
final state = ref.read(videoPlaybackValueProvider).state;
if (state == VideoPlaybackState.playing) {
ref.read(videoPlayerControlsProvider.notifier).pause();
} else {
ref.read(videoPlayerControlsProvider.notifier).play();
}
}
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: showControlsAndStartHideTimer,
child: AbsorbPointer(
absorbing: !ref.watch(showControlsProvider),
child: Stack(
children: [
if (showBuffering.value)
const Center(
child: DelayedLoadingIndicator(
fadeInDuration: Duration(milliseconds: 400),
),
)
else
GestureDetector(
onTap: () {
if (state != VideoPlaybackState.playing) {
togglePlay();
}
ref.read(showControlsProvider.notifier).show = false;
},
child: CenterPlayButton(
backgroundColor: Colors.black54,
iconColor: Colors.white,
isFinished: state == VideoPlaybackState.completed,
isPlaying: state == VideoPlaybackState.playing,
show: ref.watch(showControlsProvider),
onPressed: togglePlay,
),
),
],
),
),
);
}
}
@@ -0,0 +1,110 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/current_album.provider.dart';
import 'package:immich_mobile/modules/album/ui/add_to_album_bottom_sheet.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
import 'package:immich_mobile/modules/home/ui/upload_dialog.dart';
import 'package:immich_mobile/modules/partner/providers/partner.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/user.provider.dart';
class GalleryAppBar extends ConsumerWidget {
final Asset asset;
final void Function() showInfo;
final void Function() onToggleMotionVideo;
final bool isPlayingVideo;
const GalleryAppBar({
super.key,
required this.asset,
required this.showInfo,
required this.onToggleMotionVideo,
required this.isPlayingVideo,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final album = ref.watch(currentAlbumProvider);
final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.isarId;
final isPartner = ref
.watch(partnerSharedWithProvider)
.map((e) => e.isarId)
.contains(asset.ownerId);
toggleFavorite(Asset asset) =>
ref.read(assetProvider.notifier).toggleFavorite([asset]);
handleActivities() {
if (album != null && album.shared && album.remoteId != null) {
context.pushRoute(const ActivitiesRoute());
}
}
handleUpload(Asset asset) {
showDialog(
context: context,
builder: (BuildContext _) {
return UploadDialog(
onUpload: () {
ref
.read(manualUploadProvider.notifier)
.uploadAssets(context, [asset]);
},
);
},
);
}
addToAlbum(Asset addToAlbumAsset) {
showModalBottomSheet(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15.0),
),
context: context,
builder: (BuildContext _) {
return AddToAlbumBottomSheet(
assets: [addToAlbumAsset],
);
},
);
}
return IgnorePointer(
ignoring: !ref.watch(showControlsProvider),
child: AnimatedOpacity(
duration: const Duration(milliseconds: 100),
opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0,
child: Container(
color: Colors.black.withOpacity(0.4),
child: TopControlAppBar(
isOwner: isOwner,
isPartner: isPartner,
isPlayingMotionVideo: isPlayingVideo,
asset: asset,
onMoreInfoPressed: showInfo,
onFavorite: toggleFavorite,
onUploadPressed: asset.isLocal ? () => handleUpload(asset) : null,
onDownloadPressed: asset.isLocal
? null
: () =>
ref.read(imageViewerStateProvider.notifier).downloadAsset(
asset,
context,
),
onToggleMotionVideo: onToggleMotionVideo,
onAddToAlbumPressed: () => addToAlbum(asset),
onActivitiesPressed: handleActivities,
),
),
),
);
}
}
@@ -0,0 +1,125 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/show_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';
/// The video controls for the [videPlayerControlsProvider]
class VideoControls extends ConsumerWidget {
const VideoControls({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final duration =
ref.watch(videoPlaybackValueProvider.select((v) => v.duration));
final position =
ref.watch(videoPlaybackValueProvider.select((v) => v.position));
return AnimatedOpacity(
opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0,
duration: const Duration(milliseconds: 100),
child: OrientationBuilder(
builder: (context, orientation) => Container(
padding: EdgeInsets.symmetric(
horizontal: orientation == Orientation.portrait ? 12.0 : 64.0,
),
color: Colors.black.withOpacity(0.4),
child: Padding(
padding: MediaQuery.of(context).orientation == Orientation.portrait
? const EdgeInsets.symmetric(horizontal: 12.0)
: const EdgeInsets.symmetric(horizontal: 64.0),
child: Row(
children: [
Text(
_formatDuration(position),
style: TextStyle(
fontSize: 14.0,
color: Colors.white.withOpacity(.75),
fontWeight: FontWeight.normal,
),
),
Expanded(
child: Slider(
value: duration == Duration.zero
? 0.0
: min(
position.inMicroseconds /
duration.inMicroseconds *
100,
100,
),
min: 0,
max: 100,
thumbColor: Colors.white,
activeColor: Colors.white,
inactiveColor: Colors.white.withOpacity(0.75),
onChanged: (position) {
ref.read(videoPlayerControlsProvider.notifier).position =
position;
},
),
),
Text(
_formatDuration(duration),
style: TextStyle(
fontSize: 14.0,
color: Colors.white.withOpacity(.75),
fontWeight: FontWeight.normal,
),
),
IconButton(
icon: Icon(
ref.watch(
videoPlayerControlsProvider.select((value) => value.mute),
)
? Icons.volume_off
: Icons.volume_up,
),
onPressed: () => ref
.read(videoPlayerControlsProvider.notifier)
.toggleMute(),
color: Colors.white,
),
],
),
),
),
),
);
}
String _formatDuration(Duration position) {
final ms = position.inMilliseconds;
int seconds = ms ~/ 1000;
final int hours = seconds ~/ 3600;
seconds = seconds % 3600;
final minutes = seconds ~/ 60;
seconds = seconds % 60;
final hoursString = hours >= 10
? '$hours'
: hours == 0
? '00'
: '0$hours';
final minutesString = minutes >= 10
? '$minutes'
: minutes == 0
? '00'
: '0$minutes';
final secondsString = seconds >= 10
? '$seconds'
: seconds == 0
? '00'
: '0$seconds';
final formattedTime =
'${hoursString == '00' ? '' : '$hoursString:'}$minutesString:$secondsString';
return formattedTime;
}
}
@@ -0,0 +1,45 @@
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/asset_viewer/hooks/chewiew_controller_hook.dart';
import 'package:immich_mobile/modules/asset_viewer/ui/custom_video_player_controls.dart';
import 'package:video_player/video_player.dart';
class VideoPlayerViewer extends HookConsumerWidget {
final VideoPlayerController controller;
final bool isMotionVideo;
final Widget? placeholder;
final Duration hideControlsTimer;
final bool showControls;
final bool showDownloadingIndicator;
const VideoPlayerViewer({
super.key,
required this.controller,
required this.isMotionVideo,
this.placeholder,
required this.hideControlsTimer,
required this.showControls,
required this.showDownloadingIndicator,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final chewie = useChewieController(
controller: controller,
controlsSafeAreaMinimum: const EdgeInsets.only(
bottom: 100,
),
placeholder: SizedBox.expand(child: placeholder),
customControls: CustomVideoPlayerControls(
hideTimerDuration: hideControlsTimer,
),
showControls: showControls && !isMotionVideo,
hideControlsTimer: hideControlsTimer,
);
return Chewie(
controller: chewie,
);
}
}
@@ -1,209 +0,0 @@
import 'dart:async';
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/show_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/ui/center_play_button.dart';
import 'package:immich_mobile/shared/ui/delayed_loading_indicator.dart';
import 'package:video_player/video_player.dart';
class VideoPlayerControls extends ConsumerStatefulWidget {
const VideoPlayerControls({
super.key,
});
@override
VideoPlayerControlsState createState() => VideoPlayerControlsState();
}
class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
with SingleTickerProviderStateMixin {
late VideoPlayerController controller;
late VideoPlayerValue _latestValue;
bool _displayBufferingIndicator = false;
double? _latestVolume;
Timer? _hideTimer;
ChewieController? _chewieController;
ChewieController get chewieController => _chewieController!;
@override
Widget build(BuildContext context) {
ref.listen(videoPlayerControlsProvider.select((value) => value.mute),
(_, value) {
_mute(value);
_cancelAndRestartTimer();
});
ref.listen(videoPlayerControlsProvider.select((value) => value.position),
(_, position) {
_seekTo(position);
_cancelAndRestartTimer();
});
if (_latestValue.hasError) {
return chewieController.errorBuilder?.call(
context,
chewieController.videoPlayerController.value.errorDescription!,
) ??
const Center(
child: Icon(
Icons.error,
color: Colors.white,
size: 42,
),
);
}
return GestureDetector(
onTap: () => _cancelAndRestartTimer(),
child: AbsorbPointer(
absorbing: !ref.watch(showControlsProvider),
child: Stack(
children: [
if (_displayBufferingIndicator)
const Center(
child: DelayedLoadingIndicator(
fadeInDuration: Duration(milliseconds: 400),
),
)
else
_buildHitArea(),
],
),
),
);
}
@override
void dispose() {
_dispose();
super.dispose();
}
void _dispose() {
controller.removeListener(_updateState);
_hideTimer?.cancel();
}
@override
void didChangeDependencies() {
final oldController = _chewieController;
_chewieController = ChewieController.of(context);
controller = chewieController.videoPlayerController;
_latestValue = controller.value;
if (oldController != chewieController) {
_dispose();
_initialize();
}
super.didChangeDependencies();
}
Widget _buildHitArea() {
final bool isFinished = _latestValue.position >= _latestValue.duration;
return GestureDetector(
onTap: () {
if (!_latestValue.isPlaying) {
_playPause();
}
ref.read(showControlsProvider.notifier).show = false;
},
child: CenterPlayButton(
backgroundColor: Colors.black54,
iconColor: Colors.white,
isFinished: isFinished,
isPlaying: controller.value.isPlaying,
show: ref.watch(showControlsProvider),
onPressed: _playPause,
),
);
}
void _cancelAndRestartTimer() {
_hideTimer?.cancel();
_startHideTimer();
ref.read(showControlsProvider.notifier).show = true;
}
Future<void> _initialize() async {
ref.read(showControlsProvider.notifier).show = false;
_mute(ref.read(videoPlayerControlsProvider.select((value) => value.mute)));
_latestValue = controller.value;
controller.addListener(_updateState);
if (controller.value.isPlaying || chewieController.autoPlay) {
_startHideTimer();
}
}
void _playPause() {
final isFinished = _latestValue.position >= _latestValue.duration;
setState(() {
if (controller.value.isPlaying) {
ref.read(showControlsProvider.notifier).show = true;
_hideTimer?.cancel();
controller.pause();
} else {
_cancelAndRestartTimer();
if (!controller.value.isInitialized) {
controller.initialize().then((_) {
controller.play();
});
} else {
if (isFinished) {
controller.seekTo(Duration.zero);
}
controller.play();
}
}
});
}
void _startHideTimer() {
final hideControlsTimer = chewieController.hideControlsTimer;
_hideTimer?.cancel();
_hideTimer = Timer(hideControlsTimer, () {
ref.read(showControlsProvider.notifier).show = false;
});
}
void _updateState() {
if (!mounted) return;
_displayBufferingIndicator = controller.value.isBuffering;
setState(() {
_latestValue = controller.value;
ref.read(videoPlaybackValueProvider.notifier).value = VideoPlaybackValue(
position: _latestValue.position,
duration: _latestValue.duration,
);
});
}
void _mute(bool mute) {
if (mute) {
_latestVolume = controller.value.volume;
controller.setVolume(0);
} else {
controller.setVolume(_latestVolume ?? 0.5);
}
}
void _seekTo(double position) {
final Duration pos = controller.value.duration * (position / 100.0);
if (pos != controller.value.position) {
controller.seekTo(pos);
}
}
}