Refactors video player controller
format fixing video format Working format
This commit is contained in:
@@ -10,10 +10,12 @@ import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.provide
|
||||
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/backup/providers/manual_upload.provider.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/upload_dialog.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';
|
||||
|
||||
@@ -21,6 +23,8 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
final Asset asset;
|
||||
final bool showStack;
|
||||
final int stackIndex;
|
||||
final int totalAssets;
|
||||
final bool showVideoPlayerControls;
|
||||
final PageController controller;
|
||||
|
||||
const BottomGalleryBar({
|
||||
@@ -29,6 +33,8 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
required this.stackIndex,
|
||||
required this.asset,
|
||||
required this.controller,
|
||||
required this.totalAssets,
|
||||
required this.showVideoPlayerControls,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -40,7 +46,12 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
: <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(
|
||||
@@ -87,11 +98,10 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
ref
|
||||
.read(assetStackStateProvider(asset).notifier)
|
||||
.removeChild(stackIndex - 1);
|
||||
stackIndex.value = stackIndex.value - 1;
|
||||
}
|
||||
}
|
||||
|
||||
void handleDelete(Asset deleteAsset) async {
|
||||
void handleDelete() async {
|
||||
// Cannot delete readOnly / external assets. They are handled through library offline jobs
|
||||
if (asset.isReadOnly) {
|
||||
ImmichToast.show(
|
||||
@@ -104,7 +114,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
}
|
||||
Future<bool> onDelete(bool force) async {
|
||||
final isDeleted = await ref.read(assetProvider.notifier).deleteAssets(
|
||||
{deleteAsset},
|
||||
{asset},
|
||||
force: force,
|
||||
);
|
||||
if (isDeleted && isParent) {
|
||||
@@ -127,7 +137,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
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 && deleteAsset.isRemote && isParent) {
|
||||
if (context.mounted && asset.isRemote && isParent) {
|
||||
ImmichToast.show(
|
||||
durationInSecond: 1,
|
||||
context: context,
|
||||
@@ -178,7 +188,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
.read(assetStackServiceProvider)
|
||||
.updateStackParent(
|
||||
asset,
|
||||
stackElements.elementAt(stackIndex.value),
|
||||
stackElements.elementAt(stackIndex),
|
||||
);
|
||||
ctx.pop();
|
||||
context.popRoute();
|
||||
@@ -213,7 +223,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
await ref.read(assetStackServiceProvider).updateStack(
|
||||
asset,
|
||||
childrenToRemove: [
|
||||
stackElements.elementAt(stackIndex.value),
|
||||
stackElements.elementAt(stackIndex),
|
||||
],
|
||||
);
|
||||
removeAssetFromStack();
|
||||
@@ -273,21 +283,6 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
removeAssetFromStack();
|
||||
}
|
||||
|
||||
handleUpload(Asset asset) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext _) {
|
||||
return UploadDialog(
|
||||
onUpload: () {
|
||||
ref
|
||||
.read(manualUploadProvider.notifier)
|
||||
.uploadAssets(context, [asset]);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
handleDownload() {
|
||||
if (asset.isLocal) {
|
||||
return;
|
||||
@@ -323,17 +318,10 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0,
|
||||
child: Column(
|
||||
children: [
|
||||
if (stack.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 10,
|
||||
bottom: 30,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: buildStackedChildren(),
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: showVideoPlayerControls,
|
||||
child: const VideoControls(),
|
||||
),
|
||||
BottomNavigationBar(
|
||||
backgroundColor: Colors.black.withOpacity(0.4),
|
||||
unselectedIconTheme: const IconThemeData(color: Colors.white),
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
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,
|
||||
() {
|
||||
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;
|
||||
|
||||
// Synchronize player with video state
|
||||
if (state == VideoPlaybackState.playing) {
|
||||
ref.read(videoPlayerControlsProvider.notifier).play();
|
||||
} else if (state == VideoPlaybackState.paused) {
|
||||
ref.read(videoPlayerControlsProvider.notifier).pause();
|
||||
}
|
||||
});
|
||||
|
||||
/// Toggles between playing and pausing depending on the state of the video
|
||||
void togglePlay() {
|
||||
showControlsAndStartHideTimer();
|
||||
ref.read(videoPlayerControlsProvider.notifier).togglePlay();
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => showControlsAndStartHideTimer(),
|
||||
child: AbsorbPointer(
|
||||
absorbing: !ref.watch(showControlsProvider),
|
||||
child: Stack(
|
||||
children: [
|
||||
if (showBuffering.value)
|
||||
const Center(
|
||||
child: DelayedLoadingIndicator(
|
||||
fadeInDuration: Duration(milliseconds: 400),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (state != VideoPlaybackState.playing) {
|
||||
togglePlay();
|
||||
}
|
||||
},
|
||||
child: CenterPlayButton(
|
||||
backgroundColor: Colors.black54,
|
||||
iconColor: Colors.white,
|
||||
isFinished: state == VideoPlaybackState.completed,
|
||||
isPlaying: state == VideoPlaybackState.playing,
|
||||
show: ref.watch(showControlsProvider),
|
||||
onPressed: togglePlay,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,72 +12,78 @@ class VideoControls extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final player = ref.watch(videoPlaybackValueProvider);
|
||||
print('player is $player');
|
||||
final duration = player.duration;
|
||||
final position = player.position;
|
||||
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: Container(
|
||||
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,
|
||||
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: player.duration == Duration.zero
|
||||
? 0.0
|
||||
: min(
|
||||
player.position.inMicroseconds /
|
||||
player.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;
|
||||
},
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
),
|
||||
onPressed: () =>
|
||||
ref.read(videoPlayerControlsProvider.notifier).toggleMute(),
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user