diff --git a/lib/pages/video/widgets/player_focus.dart b/lib/pages/video/widgets/player_focus.dart index b75ca1fdd9..dcb7af8f47 100644 --- a/lib/pages/video/widgets/player_focus.dart +++ b/lib/pages/video/widgets/player_focus.dart @@ -99,21 +99,22 @@ class PlayerFocus extends StatelessWidget { if (key == LogicalKeyboardKey.arrowRight) { if (!plPlayerController.isLive) { if (event is KeyDownEvent) { - if (hasPlayer && !plPlayerController.longPressStatus.value) { + if (hasPlayer && !plPlayerController.isSeeking.value) { plPlayerController ..cancelLongPressTimer() ..longPressTimer ??= Timer( const Duration(milliseconds: 200), - () => plPlayerController - ..cancelLongPressTimer() - ..setLongPressStatus(true), + () { + plPlayerController.cancelLongPressTimer(); + plPlayerController.startSeeking(true); + }, ); } } else if (event is KeyUpEvent) { plPlayerController.cancelLongPressTimer(); if (hasPlayer) { - if (plPlayerController.longPressStatus.value) { - plPlayerController.setLongPressStatus(false); + if (plPlayerController.isSeeking.value) { + plPlayerController.endSeeking(); } else { plPlayerController.onForward( plPlayerController.fastForBackwardDuration, @@ -125,6 +126,36 @@ class PlayerFocus extends StatelessWidget { return true; } + if (key == LogicalKeyboardKey.arrowLeft) { + if (!plPlayerController.isLive) { + if (event is KeyDownEvent) { + if (hasPlayer && !plPlayerController.isSeeking.value) { + plPlayerController + ..cancelLongPressTimer() + ..longPressTimer ??= Timer( + const Duration(milliseconds: 200), + () { + plPlayerController.cancelLongPressTimer(); + plPlayerController.startSeeking(false); + }, + ); + } + } else if (event is KeyUpEvent) { + plPlayerController.cancelLongPressTimer(); + if (hasPlayer) { + if (plPlayerController.isSeeking.value) { + plPlayerController.endSeeking(); + } else { + plPlayerController.onBackward( + plPlayerController.fastForBackwardDuration, + ); + } + } + } + } + return true; + } + if (event is KeyDownEvent) { final isDigit1 = key == LogicalKeyboardKey.digit1; if (isDigit1 || key == LogicalKeyboardKey.digit2) { @@ -229,14 +260,6 @@ class PlayerFocus extends StatelessWidget { if (!plPlayerController.isLive) { switch (key) { - case LogicalKeyboardKey.arrowLeft: - if (hasPlayer) { - plPlayerController.onBackward( - plPlayerController.fastForBackwardDuration, - ); - } - return true; - case LogicalKeyboardKey.keyW: if (HardwareKeyboard.instance.isMetaPressed) { return true; diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 0d6a265ee7..478007c0a2 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -124,6 +124,15 @@ class PlPlayerController { /// 是否长按倍速 final RxBool longPressStatus = false.obs; + /// 是否在拖拽进度 + final RxBool isSeeking = false.obs; + + /// 是否在快进 + final RxBool isSeekingForward = false.obs; + + /// 拖拽进度条展示/隐藏 + final RxBool showSeekIndicator = false.obs; + /// 屏幕锁 为true时,关闭控制栏 final RxBool controlsLock = false.obs; @@ -645,6 +654,9 @@ class PlPlayerController { _isVertical = isVertical ?? false; _aid = aid; _bvid = bvid; + if (bvid != null && cid != null) { + getVideoShot(); + } this.cid = cid; _epid = epid; _seasonId = seasonId; @@ -1433,6 +1445,81 @@ class PlPlayerController { longPressTimer = null; } + DateTime? _seekStartTime; + + Duration _getSeekStep() { + int baseStep = 1; + if (videoShot case Success success) { + final data = success.response; + if (data.index.length > 1) { + baseStep = (data.index[1] - data.index[0]).ceil(); + } + } + + final seekDuration = _seekStartTime != null + ? DateTime.now().difference(_seekStartTime!) + : Duration.zero; + + int multiplier = 1; + if (seekDuration.inSeconds >= 10) { + multiplier = 10; + } else if (seekDuration.inSeconds >= 5) { + multiplier = 5; + } else if (seekDuration.inSeconds >= 2) { + multiplier = 2; + } + + double videoDurationFactor = 1.0; + if (duration.value.inMinutes > 60) { + videoDurationFactor = 4.0; + } else if (duration.value.inMinutes > 30) { + videoDurationFactor = 2.0; + } + + return Duration(seconds: (baseStep * multiplier * videoDurationFactor).round()); + } + + Timer? _seekingTimer; + void startSeeking(bool isForward) { + if (isLive || controlsLock.value || isSeeking.value) return; + + _seekStartTime = DateTime.now(); + isSeeking.value = true; + isSeekingForward.value = isForward; + showSeekIndicator.value = true; + _videoPlayerController?.pause(); + _seekingTimer?.cancel(); + _seekingTimer = Timer.periodic(const Duration(milliseconds: 200), (_) { + updateSeekPosition(isForward); + }); + } + + void updateSeekPosition(bool isForward) { + final step = _getSeekStep(); + final newPosition = isForward + ? sliderPosition.value + step + : sliderPosition.value - step; + + if (newPosition >= Duration.zero && newPosition <= duration.value) { + sliderPosition.value = newPosition; + updateSliderPositionSecond(); + } + } + + void endSeeking() { + _seekingTimer?.cancel(); + _seekStartTime = null; + if (isSeeking.value) { + seekTo(sliderPosition.value).then((_) { + play(); + }); + } + isSeeking.value = false; + Future.delayed(const Duration(seconds: 1), () { + showSeekIndicator.value = false; + }); + } + /// 设置长按倍速状态 live模式下禁用 Future setLongPressStatus(bool val) async { if (isLive) { diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index f9c86efee1..c94f9978a1 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -48,6 +48,7 @@ import 'package:PiliPlus/plugin/pl_player/widgets/common_btn.dart'; import 'package:PiliPlus/plugin/pl_player/widgets/forward_seek.dart'; import 'package:PiliPlus/plugin/pl_player/widgets/mpv_convert_webp.dart'; import 'package:PiliPlus/plugin/pl_player/widgets/play_pause_btn.dart'; +import 'package:PiliPlus/plugin/pl_player/widgets/seek_indicator.dart'; import 'package:PiliPlus/utils/duration_utils.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/id_utils.dart'; @@ -1625,6 +1626,16 @@ class _PLVideoPlayerState extends State ), ), + /// 拖拽进度条 + if (!isLive) + IgnorePointer( + ignoring: true, + child: Align( + alignment: Alignment.center, + child: SeekIndicator(controller: plPlayerController), + ), + ), + // 头部、底部控制条 Positioned.fill( top: -1, diff --git a/lib/plugin/pl_player/widgets/seek_indicator.dart b/lib/plugin/pl_player/widgets/seek_indicator.dart new file mode 100644 index 0000000000..dcc9023f40 --- /dev/null +++ b/lib/plugin/pl_player/widgets/seek_indicator.dart @@ -0,0 +1,48 @@ +import 'package:PiliPlus/utils/duration_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:PiliPlus/plugin/pl_player/controller.dart'; + +class SeekIndicator extends StatelessWidget { + final PlPlayerController controller; + + const SeekIndicator({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return Obx(() { + if (!controller.showSeekIndicator.value) { + return const SizedBox.shrink(); + } + return Material( + type: MaterialType.transparency, + child: Container( + alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Obx( + () => Icon( + controller.isSeekingForward.value + ? Icons.fast_forward + : Icons.fast_rewind, + size: 24.0, + color: Colors.white, + ), + ), + const SizedBox(height: 8.0), + Obx(() => Text( + '${DurationUtils.formatDuration(controller.sliderPosition.value.inSeconds)} / ${DurationUtils.formatDuration(controller.duration.value.inSeconds)}', + style: const TextStyle( + fontSize: 12.0, + color: Colors.white, + ), + )), + ], + ), + ), + ); + }); + } +}