wip: video preview

This commit is contained in:
kodjomoustapha 2024-10-07 16:33:35 +01:00
parent 980f1e4c28
commit 4ef1345bd4
5 changed files with 293 additions and 30 deletions

View file

@ -1054,6 +1054,7 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
tempDuration: (value) { tempDuration: (value) {
_tempPosition.value = value; _tempPosition.value = value;
}, },
video: _video.value,
) )
: MobileControllerWidget( : MobileControllerWidget(
videoController: _controller, videoController: _controller,
@ -1061,6 +1062,7 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
videoStatekey: _key, videoStatekey: _key,
bottomButtonBarWidget: _mobileBottomButtonBar(context), bottomButtonBarWidget: _mobileBottomButtonBar(context),
streamController: _streamController, streamController: _streamController,
video: _video.value,
), ),
controller: _controller, controller: _controller,
width: context.width(1), width: context.width(1),

View file

@ -9,13 +9,19 @@ class CustomSeekBar extends StatefulWidget {
final Duration? delta; final Duration? delta;
final Function(Duration)? onSeekStart; final Function(Duration)? onSeekStart;
final Function(Duration)? onSeekEnd; final Function(Duration)? onSeekEnd;
final Function(bool) isDragging;
final Function(double?) dragPosition;
final Function(Duration) onDragDuration;
const CustomSeekBar( const CustomSeekBar(
{super.key, {super.key,
this.onSeekStart, this.onSeekStart,
this.onSeekEnd, this.onSeekEnd,
required this.player, required this.player,
this.delta}); this.delta,
required this.isDragging,
required this.dragPosition,
required this.onDragDuration});
@override @override
CustomSeekBarState createState() => CustomSeekBarState(); CustomSeekBarState createState() => CustomSeekBarState();
@ -58,6 +64,27 @@ class CustomSeekBarState extends State<CustomSeekBar> {
} }
final isDesktop = Platform.isMacOS || Platform.isWindows || Platform.isLinux; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
@ -78,33 +105,56 @@ class CustomSeekBarState extends State<CustomSeekBar> {
), ),
))), ))),
Expanded( Expanded(
child: SliderTheme( child: Listener(
data: SliderTheme.of(context).copyWith( onPointerMove: (details) {
trackHeight: isDesktop ? null : 3, _onMove(details);
overlayShape: const RoundSliderOverlayShape(overlayRadius: 5.0), },
), child: MouseRegion(
child: Slider( onExit: (_) {
max: max(duration.inMilliseconds.toDouble(), 0), widget.isDragging.call(false);
value: max( widget.dragPosition.call(null);
(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());
});
}
}, },
onChangeEnd: (value) async { onHover: (details) {
widget.onSeekEnd?.call(Duration( _onMove(details);
milliseconds: value.toInt() - position.inMilliseconds));
widget.player.seek(Duration(milliseconds: value.toInt()));
}, },
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()));
},
),
),
), ),
), ),
), ),

View file

@ -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/providers/anime_player_controller_provider.dart';
import 'package:mangayomi/modules/anime/widgets/custom_seekbar.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/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: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.dart';
import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.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<VideoState> videoStatekey; final GlobalKey<VideoState> videoStatekey;
final Widget bottomButtonBarWidget; final Widget bottomButtonBarWidget;
final Widget seekToWidget; final Widget seekToWidget;
final VideoPrefs? video;
const DesktopControllerWidget( const DesktopControllerWidget(
{super.key, {super.key,
required this.videoController, required this.videoController,
@ -29,7 +31,8 @@ class DesktopControllerWidget extends StatefulWidget {
required this.streamController, required this.streamController,
required this.videoStatekey, required this.videoStatekey,
required this.seekToWidget, required this.seekToWidget,
required this.tempDuration}); required this.tempDuration,
required this.video});
@override @override
State<DesktopControllerWidget> createState() => State<DesktopControllerWidget> createState() =>
@ -42,6 +45,9 @@ class _DesktopControllerWidgetState extends State<DesktopControllerWidget> {
Duration controlsTransitionDuration = const Duration(milliseconds: 300); Duration controlsTransitionDuration = const Duration(milliseconds: 300);
Color backdropColor = const Color(0x66000000); Color backdropColor = const Color(0x66000000);
Timer? _timer; Timer? _timer;
final ValueNotifier<bool> _isDragging = ValueNotifier(false);
final ValueNotifier<double?> _dragPosition = ValueNotifier(null);
final ValueNotifier<Duration?> _onDragDuration = ValueNotifier(null);
int swipeDuration = 0; // Duration to seek in video int swipeDuration = 0; // Duration to seek in video
bool showSwipeDuration = false; // Whether to show the seek duration overlay bool showSwipeDuration = false; // Whether to show the seek duration overlay
@ -374,6 +380,21 @@ class _DesktopControllerWidgetState extends State<DesktopControllerWidget> {
widget.tempDuration(null); widget.tempDuration(null);
}, },
player: widget.videoController.player, 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<DesktopControllerWidget> {
), ),
), ),
), ),
VideoPreview(
isDragging: _isDragging,
dragPosition: _dragPosition,
onDragDuration: _onDragDuration,
video: widget.video!),
], ],
), ),
); );

View file

@ -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/custom_seekbar.dart';
import 'package:mangayomi/modules/anime/widgets/indicator_builder.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/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/manga/reader/providers/push_router.dart';
import 'package:mangayomi/modules/more/settings/player/providers/player_state_provider.dart'; import 'package:mangayomi/modules/more/settings/player/providers/player_state_provider.dart';
import 'package:volume_controller/volume_controller.dart'; import 'package:volume_controller/volume_controller.dart';
@ -21,13 +22,15 @@ class MobileControllerWidget extends ConsumerStatefulWidget {
final Widget topButtonBarWidget; final Widget topButtonBarWidget;
final GlobalKey<VideoState> videoStatekey; final GlobalKey<VideoState> videoStatekey;
final Widget bottomButtonBarWidget; final Widget bottomButtonBarWidget;
final VideoPrefs? video;
const MobileControllerWidget( const MobileControllerWidget(
{super.key, {super.key,
required this.videoController, required this.videoController,
required this.topButtonBarWidget, required this.topButtonBarWidget,
required this.bottomButtonBarWidget, required this.bottomButtonBarWidget,
required this.streamController, required this.streamController,
required this.videoStatekey}); required this.videoStatekey,
required this.video});
@override @override
ConsumerState<MobileControllerWidget> createState() => ConsumerState<MobileControllerWidget> createState() =>
@ -49,6 +52,10 @@ class _MobileControllerWidgetState
final ValueNotifier<double> _volumeValue = ValueNotifier(0.0); final ValueNotifier<double> _volumeValue = ValueNotifier(0.0);
final ValueNotifier<bool> _volumeIndicator = ValueNotifier(false); final ValueNotifier<bool> _volumeIndicator = ValueNotifier(false);
final ValueNotifier<bool> _isDragging = ValueNotifier(false);
final ValueNotifier<double?> _dragPosition = ValueNotifier(null);
final ValueNotifier<Duration?> _onDragDuration = ValueNotifier(null);
Timer? _volumeTimer; Timer? _volumeTimer;
// The default event stream in package:volume_controller is buggy. // The default event stream in package:volume_controller is buggy.
bool _volumeInterceptEventStream = false; bool _volumeInterceptEventStream = false;
@ -438,6 +445,21 @@ class _MobileControllerWidgetState
}); });
}, },
player: widget.videoController.player, player: widget.videoController.player,
isDragging: (value) {
setState(() {
_isDragging.value = value;
});
},
dragPosition: (value) {
setState(() {
_dragPosition.value = value;
});
},
onDragDuration: (value) {
setState(() {
_onDragDuration.value = value;
});
},
), ),
), ),
widget.bottomButtonBarWidget widget.bottomButtonBarWidget
@ -463,8 +485,12 @@ class _MobileControllerWidgetState
Padding( Padding(
padding: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.only(bottom: 10),
child: CustomSeekBar( child: CustomSeekBar(
delta: _seekBarDeltaValueNotifier, delta: _seekBarDeltaValueNotifier,
player: widget.videoController.player), 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!),
], ],
); );
} }

View file

@ -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<TestTest> createState() => _TestTestState();
}
class _TestTestState extends State<TestTest> {
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<PlatformVideoController?>(
valueListenable: _controller.notifier,
builder: (context, notifier, _) => notifier == null
? const SizedBox.shrink()
: ValueListenableBuilder<int?>(
valueListenable: notifier.id,
builder: (context, id, _) {
return ValueListenableBuilder<Rect?>(
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<bool> isDragging;
final ValueNotifier<double?> dragPosition;
final ValueNotifier<Duration?> 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();
},
);
},
);
},
);
}
}