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

View file

@ -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<CustomSeekBar> {
}
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<CustomSeekBar> {
),
))),
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()));
},
),
),
),
),
),

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/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<VideoState> 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<DesktopControllerWidget> createState() =>
@ -42,6 +45,9 @@ class _DesktopControllerWidgetState extends State<DesktopControllerWidget> {
Duration controlsTransitionDuration = const Duration(milliseconds: 300);
Color backdropColor = const Color(0x66000000);
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
bool showSwipeDuration = false; // Whether to show the seek duration overlay
@ -374,6 +380,21 @@ class _DesktopControllerWidgetState extends State<DesktopControllerWidget> {
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<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/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<VideoState> 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<MobileControllerWidget> createState() =>
@ -49,6 +52,10 @@ class _MobileControllerWidgetState
final ValueNotifier<double> _volumeValue = ValueNotifier(0.0);
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;
// 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!),
],
);
}

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();
},
);
},
);
},
);
}
}