From 4ef1345bd42161f7422ec685e32fea58c1fb5d35 Mon Sep 17 00:00:00 2001 From: kodjomoustapha <107993382+kodjodevf@users.noreply.github.com> Date: Mon, 7 Oct 2024 16:33:35 +0100 Subject: [PATCH] wip: video preview --- lib/modules/anime/anime_player_view.dart | 2 + lib/modules/anime/widgets/custom_seekbar.dart | 102 +++++++++--- lib/modules/anime/widgets/desktop.dart | 28 +++- lib/modules/anime/widgets/mobile.dart | 37 ++++- lib/modules/anime/widgets/video_preview.dart | 154 ++++++++++++++++++ 5 files changed, 293 insertions(+), 30 deletions(-) create mode 100644 lib/modules/anime/widgets/video_preview.dart diff --git a/lib/modules/anime/anime_player_view.dart b/lib/modules/anime/anime_player_view.dart index 78b649bb..87e1d3b5 100644 --- a/lib/modules/anime/anime_player_view.dart +++ b/lib/modules/anime/anime_player_view.dart @@ -1054,6 +1054,7 @@ class _AnimeStreamPageState extends riv.ConsumerState tempDuration: (value) { _tempPosition.value = value; }, + video: _video.value, ) : MobileControllerWidget( videoController: _controller, @@ -1061,6 +1062,7 @@ class _AnimeStreamPageState extends riv.ConsumerState videoStatekey: _key, bottomButtonBarWidget: _mobileBottomButtonBar(context), streamController: _streamController, + video: _video.value, ), controller: _controller, width: context.width(1), diff --git a/lib/modules/anime/widgets/custom_seekbar.dart b/lib/modules/anime/widgets/custom_seekbar.dart index 5cc1a1ae..c1ef3104 100644 --- a/lib/modules/anime/widgets/custom_seekbar.dart +++ b/lib/modules/anime/widgets/custom_seekbar.dart @@ -9,13 +9,19 @@ class CustomSeekBar extends StatefulWidget { final Duration? delta; final Function(Duration)? onSeekStart; final Function(Duration)? onSeekEnd; + final Function(bool) isDragging; + final Function(double?) dragPosition; + final Function(Duration) onDragDuration; const CustomSeekBar( {super.key, this.onSeekStart, this.onSeekEnd, required this.player, - this.delta}); + this.delta, + required this.isDragging, + required this.dragPosition, + required this.onDragDuration}); @override CustomSeekBarState createState() => CustomSeekBarState(); @@ -58,6 +64,27 @@ class CustomSeekBarState extends State { } final isDesktop = Platform.isMacOS || Platform.isWindows || Platform.isLinux; + final GlobalKey _sliderKey = GlobalKey(); + + void _onMove(PointerEvent details) { + final RenderBox box = + _sliderKey.currentContext!.findRenderObject() as RenderBox; + final Offset localOffset = box.globalToLocal(details.position); + + if (localOffset.dx >= 0 && localOffset.dx <= box.size.width) { + final pourcentage = (localOffset.dx.ceil() / box.size.width.ceil()) * 100; + widget.onDragDuration.call( + Duration(seconds: ((pourcentage / 100) * duration.inSeconds).ceil())); + widget.isDragging.call(true); + widget.dragPosition.call(localOffset.dx); + Future.delayed(const Duration(milliseconds: 50)) + .then((e) => widget.isDragging(false)); + } else { + widget.isDragging.call(true); + widget.dragPosition.call(null); + } + } + @override Widget build(BuildContext context) { return SizedBox( @@ -78,33 +105,56 @@ class CustomSeekBarState extends State { ), ))), Expanded( - child: SliderTheme( - data: SliderTheme.of(context).copyWith( - trackHeight: isDesktop ? null : 3, - overlayShape: const RoundSliderOverlayShape(overlayRadius: 5.0), - ), - child: Slider( - max: max(duration.inMilliseconds.toDouble(), 0), - value: max( - (widget.delta ?? tempPosition ?? position) - .inMilliseconds - .toDouble(), - 0), - secondaryTrackValue: max(buffer.inMilliseconds.toDouble(), 0), - onChanged: (value) { - widget.onSeekStart?.call(Duration( - milliseconds: value.toInt() - position.inMilliseconds)); - if (mounted) { - setState(() { - tempPosition = Duration(milliseconds: value.toInt()); - }); - } + child: Listener( + onPointerMove: (details) { + _onMove(details); + }, + child: MouseRegion( + onExit: (_) { + widget.isDragging.call(false); + widget.dragPosition.call(null); }, - onChangeEnd: (value) async { - widget.onSeekEnd?.call(Duration( - milliseconds: value.toInt() - position.inMilliseconds)); - widget.player.seek(Duration(milliseconds: value.toInt())); + onHover: (details) { + _onMove(details); }, + child: SliderTheme( + data: SliderTheme.of(context).copyWith( + trackHeight: isDesktop ? null : 3, + overlayShape: + const RoundSliderOverlayShape(overlayRadius: 5.0), + ), + child: Slider( + key: _sliderKey, + max: max(duration.inMilliseconds.toDouble(), 0), + value: max( + (widget.delta ?? tempPosition ?? position) + .inMilliseconds + .toDouble(), + 0), + secondaryTrackValue: + max(buffer.inMilliseconds.toDouble(), 0), + onChanged: (value) { + widget.onSeekStart?.call(Duration( + milliseconds: + value.toInt() - position.inMilliseconds)); + if (mounted) { + setState(() { + tempPosition = Duration(milliseconds: value.toInt()); + }); + } + }, + onChangeStart: (value) { + widget.isDragging.call(true); + }, + onChangeEnd: (value) async { + widget.isDragging.call(false); + widget.onSeekEnd?.call(Duration( + milliseconds: + value.toInt() - position.inMilliseconds)); + widget.player.seek(Duration(milliseconds: value.toInt())); + }, + ), + ), ), ), ), diff --git a/lib/modules/anime/widgets/desktop.dart b/lib/modules/anime/widgets/desktop.dart index 409ab289..f0d43b27 100644 --- a/lib/modules/anime/widgets/desktop.dart +++ b/lib/modules/anime/widgets/desktop.dart @@ -8,6 +8,7 @@ import 'package:mangayomi/modules/anime/anime_player_view.dart'; import 'package:mangayomi/modules/anime/providers/anime_player_controller_provider.dart'; import 'package:mangayomi/modules/anime/widgets/custom_seekbar.dart'; import 'package:mangayomi/modules/anime/widgets/subtitle_view.dart'; +import 'package:mangayomi/modules/anime/widgets/video_preview.dart'; import 'package:mangayomi/modules/more/settings/player/providers/player_state_provider.dart'; import 'package:media_kit_video/media_kit_video.dart'; import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart'; @@ -21,6 +22,7 @@ class DesktopControllerWidget extends StatefulWidget { final GlobalKey videoStatekey; final Widget bottomButtonBarWidget; final Widget seekToWidget; + final VideoPrefs? video; const DesktopControllerWidget( {super.key, required this.videoController, @@ -29,7 +31,8 @@ class DesktopControllerWidget extends StatefulWidget { required this.streamController, required this.videoStatekey, required this.seekToWidget, - required this.tempDuration}); + required this.tempDuration, + required this.video}); @override State createState() => @@ -42,6 +45,9 @@ class _DesktopControllerWidgetState extends State { Duration controlsTransitionDuration = const Duration(milliseconds: 300); Color backdropColor = const Color(0x66000000); Timer? _timer; + final ValueNotifier _isDragging = ValueNotifier(false); + final ValueNotifier _dragPosition = ValueNotifier(null); + final ValueNotifier _onDragDuration = ValueNotifier(null); int swipeDuration = 0; // Duration to seek in video bool showSwipeDuration = false; // Whether to show the seek duration overlay @@ -374,6 +380,21 @@ class _DesktopControllerWidgetState extends State { widget.tempDuration(null); }, player: widget.videoController.player, + isDragging: (value) { + setState(() { + _isDragging.value = value; + }); + }, + dragPosition: (value) { + setState(() { + _dragPosition.value = value; + }); + }, + onDragDuration: (value) { + setState(() { + _onDragDuration.value = value; + }); + }, ), ), ), @@ -439,6 +460,11 @@ class _DesktopControllerWidgetState extends State { ), ), ), + VideoPreview( + isDragging: _isDragging, + dragPosition: _dragPosition, + onDragDuration: _onDragDuration, + video: widget.video!), ], ), ); diff --git a/lib/modules/anime/widgets/mobile.dart b/lib/modules/anime/widgets/mobile.dart index 1960e455..67cca30c 100644 --- a/lib/modules/anime/widgets/mobile.dart +++ b/lib/modules/anime/widgets/mobile.dart @@ -7,6 +7,7 @@ import 'package:mangayomi/modules/anime/providers/anime_player_controller_provid import 'package:mangayomi/modules/anime/widgets/custom_seekbar.dart'; import 'package:mangayomi/modules/anime/widgets/indicator_builder.dart'; import 'package:mangayomi/modules/anime/widgets/subtitle_view.dart'; +import 'package:mangayomi/modules/anime/widgets/video_preview.dart'; import 'package:mangayomi/modules/manga/reader/providers/push_router.dart'; import 'package:mangayomi/modules/more/settings/player/providers/player_state_provider.dart'; import 'package:volume_controller/volume_controller.dart'; @@ -21,13 +22,15 @@ class MobileControllerWidget extends ConsumerStatefulWidget { final Widget topButtonBarWidget; final GlobalKey videoStatekey; final Widget bottomButtonBarWidget; + final VideoPrefs? video; const MobileControllerWidget( {super.key, required this.videoController, required this.topButtonBarWidget, required this.bottomButtonBarWidget, required this.streamController, - required this.videoStatekey}); + required this.videoStatekey, + required this.video}); @override ConsumerState createState() => @@ -49,6 +52,10 @@ class _MobileControllerWidgetState final ValueNotifier _volumeValue = ValueNotifier(0.0); final ValueNotifier _volumeIndicator = ValueNotifier(false); + + final ValueNotifier _isDragging = ValueNotifier(false); + final ValueNotifier _dragPosition = ValueNotifier(null); + final ValueNotifier _onDragDuration = ValueNotifier(null); Timer? _volumeTimer; // The default event stream in package:volume_controller is buggy. bool _volumeInterceptEventStream = false; @@ -438,6 +445,21 @@ class _MobileControllerWidgetState }); }, player: widget.videoController.player, + isDragging: (value) { + setState(() { + _isDragging.value = value; + }); + }, + dragPosition: (value) { + setState(() { + _dragPosition.value = value; + }); + }, + onDragDuration: (value) { + setState(() { + _onDragDuration.value = value; + }); + }, ), ), widget.bottomButtonBarWidget @@ -463,8 +485,12 @@ class _MobileControllerWidgetState Padding( padding: const EdgeInsets.only(bottom: 10), child: CustomSeekBar( - delta: _seekBarDeltaValueNotifier, - player: widget.videoController.player), + delta: _seekBarDeltaValueNotifier, + player: widget.videoController.player, + isDragging: (value) {}, + dragPosition: (value) {}, + onDragDuration: (value) {}, + ), ), ], ), @@ -662,6 +688,11 @@ class _MobileControllerWidgetState ], ), ), + VideoPreview( + isDragging: _isDragging, + dragPosition: _dragPosition, + onDragDuration: _onDragDuration, + video: widget.video!), ], ); } diff --git a/lib/modules/anime/widgets/video_preview.dart b/lib/modules/anime/widgets/video_preview.dart new file mode 100644 index 00000000..df19619b --- /dev/null +++ b/lib/modules/anime/widgets/video_preview.dart @@ -0,0 +1,154 @@ +import 'package:flutter/material.dart'; +import 'package:mangayomi/modules/anime/anime_player_view.dart'; +import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; +import 'package:media_kit/media_kit.dart'; +import 'package:media_kit_video/media_kit_video.dart'; +import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart'; + +class TestTest extends StatefulWidget { + final Duration? onDragDuration; + final VideoPrefs video; + const TestTest( + {super.key, required this.onDragDuration, required this.video}); + + @override + State createState() => _TestTestState(); +} + +class _TestTestState extends State { + late final Player _player = + Player(configuration: const PlayerConfiguration()); + late final VideoController _controller = VideoController(_player); + + @override + void initState() { + _player.open( + Media(widget.video.videoTrack!.id, + httpHeaders: widget.video.headers, start: widget.onDragDuration), + play: false); + super.initState(); + } + + @override + void dispose() { + _player.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Positioned.fill( + child: ValueListenableBuilder( + valueListenable: _controller.notifier, + builder: (context, notifier, _) => notifier == null + ? const SizedBox.shrink() + : ValueListenableBuilder( + valueListenable: notifier.id, + builder: (context, id, _) { + return ValueListenableBuilder( + valueListenable: notifier.rect, + builder: (context, rect, _) { + if (id != null && rect != null) { + return Texture(textureId: id); + } + return const SizedBox.shrink(); + }, + ); + }, + ), + ), + ); + } +} + +class VideoPreview extends StatelessWidget { + final ValueNotifier isDragging; + final ValueNotifier dragPosition; + final ValueNotifier onDragDuration; + final VideoPrefs video; + const VideoPreview( + {super.key, + required this.isDragging, + required this.dragPosition, + required this.onDragDuration, + required this.video}); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: isDragging, + builder: (context, isDragging, child) { + return ValueListenableBuilder( + valueListenable: dragPosition, + builder: (context, dragPosition, child) { + return ValueListenableBuilder( + valueListenable: onDragDuration, + builder: (context, onDragDuration, child) { + if (dragPosition != null) { + return Positioned( + bottom: 100, + left: dragPosition >= (context.width(1) - 150) + ? null + : dragPosition >= 50 + ? dragPosition - 50 + : 0, + right: dragPosition >= (context.width(1) - 150) ? 0 : null, + child: Material( + elevation: 8.0, + borderRadius: BorderRadius.circular(8.0), + child: Container( + width: 200, + height: 100, + color: Colors.black, + child: Stack( + children: [ + if (!isDragging) + TestTest( + onDragDuration: onDragDuration, + video: video), + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Text( + onDragDuration! + .label(reference: onDragDuration), + style: const TextStyle( + color: Colors.white, + fontSize: 30, + fontWeight: FontWeight.bold, + shadows: [ + Shadow( + offset: Offset(-1.5, -1.5), + color: Colors.black, + blurRadius: 1.2), + Shadow( + offset: Offset(1.5, -1.5), + color: Colors.black, + blurRadius: 1.2), + Shadow( + offset: Offset(1.5, 1.5), + color: Colors.black, + blurRadius: 1.2), + Shadow( + offset: Offset(-1.5, 1.5), + color: Colors.black, + blurRadius: 1.2) + ], + ), + textAlign: TextAlign.center), + ) + ], + )), + ), + ); + } + return const SizedBox.shrink(); + }, + ); + }, + ); + }, + ); + } +}