From f372c26bb94c09f176206f6446e2a261a63ac63e Mon Sep 17 00:00:00 2001 From: PartyDonut <42371342+PartyDonut@users.noreply.github.com> Date: Sat, 26 Oct 2024 15:05:33 +0200 Subject: [PATCH] feature(Desktop: Added seek indicator (#80) Co-authored-by: PartyDonut --- .../video_player_seek_indicator.dart | 120 ++++++++++++++++++ .../video_player/video_player_controls.dart | 8 +- 2 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 lib/screens/video_player/components/video_player_seek_indicator.dart diff --git a/lib/screens/video_player/components/video_player_seek_indicator.dart b/lib/screens/video_player/components/video_player_seek_indicator.dart new file mode 100644 index 0000000..14f3ba5 --- /dev/null +++ b/lib/screens/video_player/components/video_player_seek_indicator.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:async/async.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fladder/providers/video_player_provider.dart'; +import 'package:fladder/util/localization_helper.dart'; + +class VideoPlayerSeekIndicator extends ConsumerStatefulWidget { + const VideoPlayerSeekIndicator({super.key}); + + @override + ConsumerState createState() => _VideoPlayerSeekIndicatorState(); +} + +class _VideoPlayerSeekIndicatorState extends ConsumerState { + RestartableTimer? timer; + + bool visible = false; + int seekPosition = 0; + + @override + void initState() { + super.initState(); + ServicesBinding.instance.keyboard.addHandler(_onKey); + } + + @override + void dispose() { + ServicesBinding.instance.keyboard.removeHandler(_onKey); + super.dispose(); + } + + void onSeekEnd() { + setState(() { + visible = false; + }); + timer?.cancel(); + timer = null; + if (seekPosition == 0) return; + final mediaPlayback = ref.read(mediaPlaybackProvider); + final newPosition = (mediaPlayback.position.inSeconds + seekPosition).clamp(0, mediaPlayback.duration.inSeconds); + ref.read(videoPlayerProvider).seek(Duration(seconds: newPosition)); + } + + void onSeekStart(int value) { + if (timer == null) { + timer = RestartableTimer(const Duration(seconds: 2), () => onSeekEnd()); + setState(() { + seekPosition = 0; + }); + } else { + timer?.reset(); + } + setState(() { + visible = true; + seekPosition += value; + }); + } + + bool _onKey(KeyEvent value) { + if (value is KeyRepeatEvent) { + if (value.logicalKey == LogicalKeyboardKey.arrowLeft) { + seekBack(); + return true; + } + if (value.logicalKey == LogicalKeyboardKey.arrowRight) { + seekForward(); + return true; + } + } + if (value is KeyDownEvent) { + if (value.logicalKey == LogicalKeyboardKey.arrowLeft) { + seekBack(); + return true; + } + if (value.logicalKey == LogicalKeyboardKey.arrowRight) { + seekForward(); + return true; + } + } + return false; + } + + @override + Widget build(BuildContext context) { + return IgnorePointer( + child: AnimatedOpacity( + duration: const Duration(milliseconds: 500), + opacity: (visible && seekPosition != 0) ? 1 : 0, + child: Center( + child: Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.85), + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + seekPosition > 0 + ? "+$seekPosition ${context.localized.seconds(seekPosition)}" + : "$seekPosition ${context.localized.seconds(seekPosition)}", + style: Theme.of(context).textTheme.bodyMedium, + ) + ], + ), + ), + ), + ), + ), + ); + } + + void seekBack({int seconds = -10}) => onSeekStart(seconds); + void seekForward({int seconds = 30}) => onSeekStart(seconds); +} diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index 32ef24c..475363c 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -23,6 +23,7 @@ import 'package:fladder/screens/shared/default_titlebar.dart'; import 'package:fladder/screens/video_player/components/video_playback_information.dart'; import 'package:fladder/screens/video_player/components/video_player_controls_extras.dart'; import 'package:fladder/screens/video_player/components/video_player_options_sheet.dart'; +import 'package:fladder/screens/video_player/components/video_player_seek_indicator.dart'; import 'package:fladder/screens/video_player/components/video_progress_bar.dart'; import 'package:fladder/screens/video_player/components/video_subtitles.dart'; import 'package:fladder/screens/video_player/components/video_volume_slider.dart'; @@ -86,12 +87,6 @@ class _DesktopControlsState extends ConsumerState { if (value.logicalKey == LogicalKeyboardKey.space) { ref.read(videoPlayerProvider).playOrPause(); } - if (value.logicalKey == LogicalKeyboardKey.arrowLeft) { - seekBack(ref); - } - if (value.logicalKey == LogicalKeyboardKey.arrowRight) { - seekForward(ref); - } if (value.logicalKey == LogicalKeyboardKey.keyF) { toggleFullScreen(ref); } @@ -172,6 +167,7 @@ class _DesktopControlsState extends ConsumerState { ), ), ), + const VideoPlayerSeekIndicator(), Consumer( builder: (context, ref, child) { final position = ref.watch(mediaPlaybackProvider.select((value) => value.position));