mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-01-11 22:40:36 +00:00
wip: video preview
This commit is contained in:
parent
980f1e4c28
commit
4ef1345bd4
5 changed files with 293 additions and 30 deletions
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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!),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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!),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
154
lib/modules/anime/widgets/video_preview.dart
Normal file
154
lib/modules/anime/widgets/video_preview.dart
Normal 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();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue