From f2cd413de2e8aabc243ec3b68c43254933c464d9 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:52:12 +0000 Subject: [PATCH 1/5] feat: Implement DPAD timeline scrubbing on long-press This commit changes the behavior of long-pressing the left and right DPAD keys on the video playback screen. Instead of adjusting the playback speed, it now scrubs the video timeline. - The video pauses when a long-press is initiated. - A visual indicator shows the current seek position and the total duration of the video. - The indicator displays a fast-forward or fast-rewind icon depending on the direction of the seek. - Releasing the DPAD key seeks to the new position and resumes playback. --- lib/pages/video/widgets/player_focus.dart | 51 ++++++++++++++----- lib/plugin/pl_player/controller.dart | 49 ++++++++++++++++++ lib/plugin/pl_player/view.dart | 11 ++++ .../pl_player/widgets/seek_indicator.dart | 48 +++++++++++++++++ 4 files changed, 145 insertions(+), 14 deletions(-) create mode 100644 lib/plugin/pl_player/widgets/seek_indicator.dart 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..9bddea0d09 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; @@ -1433,6 +1442,46 @@ class PlPlayerController { longPressTimer = null; } + Timer? _seekingTimer; + void startSeeking(bool isForward) { + if (isLive || controlsLock.value || isSeeking.value) return; + + 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 newPosition = isForward + ? sliderPosition.value + const Duration(seconds: 1) + : sliderPosition.value - const Duration(seconds: 1); + + if (newPosition >= Duration.zero && newPosition <= duration.value) { + sliderPosition.value = newPosition; + updateSliderPositionSecond(); + } + } + + void endSeeking() { + _seekingTimer?.cancel(); + if (isSeeking.value) { + seekTo(sliderPosition.value).then((_) { + if (playerStatus.value != PlayerStatus.playing) { + 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..7b571f4f4c --- /dev/null +++ b/lib/plugin/pl_player/widgets/seek_indicator.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:PiliPlus/plugin/pl_player/controller.dart'; +import 'package:PiliPlus/plugin/pl_player/models/duration.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( + '${controller.sliderPosition.value.format()} / ${controller.duration.value.format()}', + style: const TextStyle( + fontSize: 12.0, + color: Colors.white, + ), + )), + ], + ), + ), + ); + }); + } +} From fe40bfc715ce7317314156f007edf9f51f3169cf Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:59:55 +0000 Subject: [PATCH 2/5] feat: Implement DPAD timeline scrubbing on long-press This commit changes the behavior of long-pressing the left and right DPAD keys on the video playback screen. Instead of adjusting the playback speed, it now scrubs the video timeline. - The video pauses when a long-press is initiated. - A visual indicator shows the current seek position and the total duration of the video. - The indicator displays a fast-forward or fast-rewind icon depending on the direction of the seek. - Releasing the DPAD key seeks to the new position and resumes playback. - Fixes a CI build failure by using the correct duration formatting utility. --- lib/plugin/pl_player/widgets/seek_indicator.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/plugin/pl_player/widgets/seek_indicator.dart b/lib/plugin/pl_player/widgets/seek_indicator.dart index 7b571f4f4c..dcc9023f40 100644 --- a/lib/plugin/pl_player/widgets/seek_indicator.dart +++ b/lib/plugin/pl_player/widgets/seek_indicator.dart @@ -1,7 +1,7 @@ +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'; -import 'package:PiliPlus/plugin/pl_player/models/duration.dart'; class SeekIndicator extends StatelessWidget { final PlPlayerController controller; @@ -33,7 +33,7 @@ class SeekIndicator extends StatelessWidget { ), const SizedBox(height: 8.0), Obx(() => Text( - '${controller.sliderPosition.value.format()} / ${controller.duration.value.format()}', + '${DurationUtils.formatDuration(controller.sliderPosition.value.inSeconds)} / ${DurationUtils.formatDuration(controller.duration.value.inSeconds)}', style: const TextStyle( fontSize: 12.0, color: Colors.white, From 6c5eacf741c67df2c34901fc8d093977bf67d574 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 08:20:10 +0000 Subject: [PATCH 3/5] feat: Implement progressive DPAD timeline scrubbing This commit enhances the DPAD timeline scrubbing feature by making the seek step progressive. The longer the left or right DPAD key is held, the larger the seek step becomes, allowing for faster scrubbing over long distances. - A timer now tracks the duration of the long-press. - The seek step size increases at predefined intervals (e.g., after 2, 5, and 10 seconds). - This provides a better user experience for both short and long-distance seeking. --- lib/plugin/pl_player/controller.dart | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 9bddea0d09..81c302a473 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -1442,10 +1442,29 @@ class PlPlayerController { longPressTimer = null; } + DateTime? _seekStartTime; + + Duration _getSeekStep() { + if (_seekStartTime == null) { + return const Duration(seconds: 1); + } + final seekDuration = DateTime.now().difference(_seekStartTime!); + if (seekDuration.inSeconds < 2) { + return const Duration(seconds: 1); + } else if (seekDuration.inSeconds < 5) { + return const Duration(seconds: 2); + } else if (seekDuration.inSeconds < 10) { + return const Duration(seconds: 5); + } else { + return const Duration(seconds: 10); + } + } + 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; @@ -1457,9 +1476,10 @@ class PlPlayerController { } void updateSeekPosition(bool isForward) { + final step = _getSeekStep(); final newPosition = isForward - ? sliderPosition.value + const Duration(seconds: 1) - : sliderPosition.value - const Duration(seconds: 1); + ? sliderPosition.value + step + : sliderPosition.value - step; if (newPosition >= Duration.zero && newPosition <= duration.value) { sliderPosition.value = newPosition; @@ -1469,6 +1489,7 @@ class PlPlayerController { void endSeeking() { _seekingTimer?.cancel(); + _seekStartTime = null; if (isSeeking.value) { seekTo(sliderPosition.value).then((_) { if (playerStatus.value != PlayerStatus.playing) { From bdf4f64a3201e61c288f8f8d8f558798340967cb Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 09:35:55 +0000 Subject: [PATCH 4/5] feat: Implement progressive DPAD timeline scrubbing This commit enhances the DPAD timeline scrubbing feature by making the seek step progressive and addressing several user feedback points. The longer the left or right DPAD key is held, the larger the seek step becomes, allowing for faster scrubbing over long distances, especially in longer videos. - Implemented a progressive seek algorithm where the step size increases based on how long the key is held and the total duration of the video. - The base seek step is now dynamically determined by the video's preview thumbnail interval. - Fixed a bug where video playback would not resume after a seek operation was completed. - Added a more robust check to prevent errors when calculating the base seek step from video preview data. --- lib/plugin/pl_player/controller.dart | 47 +++++++++++++++++++--------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 81c302a473..07aa3309dc 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -654,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; @@ -1445,19 +1448,35 @@ class PlPlayerController { DateTime? _seekStartTime; Duration _getSeekStep() { - if (_seekStartTime == null) { - return const Duration(seconds: 1); - } - final seekDuration = DateTime.now().difference(_seekStartTime!); - if (seekDuration.inSeconds < 2) { - return const Duration(seconds: 1); - } else if (seekDuration.inSeconds < 5) { - return const Duration(seconds: 2); - } else if (seekDuration.inSeconds < 10) { - return const Duration(seconds: 5); - } else { - return const Duration(seconds: 10); + int baseStep = 1; + if (videoShot case Success success) { + final data = success.response; + if (data.index.length > 1) { + baseStep = (data.index[1] - data.index[0]).toInt(); + } + } + + 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; @@ -1492,9 +1511,7 @@ class PlPlayerController { _seekStartTime = null; if (isSeeking.value) { seekTo(sliderPosition.value).then((_) { - if (playerStatus.value != PlayerStatus.playing) { - play(); - } + play(); }); } isSeeking.value = false; From 42ac1d142d7fc24af44a845b5a606d99316d875f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 16 Dec 2025 14:03:38 +0000 Subject: [PATCH 5/5] fix: Correct progressive seek calculation and ensure playback resumes This commit fixes a bug where the progressive seek functionality would stall if the video's thumbnail interval was less than one second. It also ensures that video playback reliably resumes after any seek operation. - The progressive seek calculation now uses `ceil()` to round up the base step, preventing it from ever being zero. - The `endSeeking` method has been verified to unconditionally resume playback after a seek, fixing a previous bug where the video would remain paused. --- lib/plugin/pl_player/controller.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 07aa3309dc..478007c0a2 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -1452,7 +1452,7 @@ class PlPlayerController { if (videoShot case Success success) { final data = success.response; if (data.index.length > 1) { - baseStep = (data.index[1] - data.index[0]).toInt(); + baseStep = (data.index[1] - data.index[0]).ceil(); } }