From 8650c96b7ba83eeaaf55d78624fc3b2184ef6e65 Mon Sep 17 00:00:00 2001 From: My-Responsitories <107370289+My-Responsitories@users.noreply.github.com> Date: Fri, 17 Oct 2025 14:37:25 +0800 Subject: [PATCH] feat: danmaku seekTo (#1603) --- assets/images/dm_tip/player_dm_tip_center.svg | 1 - assets/images/dm_tip/player_dm_tip_left.svg | 1 - assets/images/dm_tip/player_dm_tip_right.svg | 1 - lib/plugin/pl_player/controller.dart | 2 +- lib/plugin/pl_player/view.dart | 346 ++++++++++-------- lib/utils/storage_pref.dart | 2 +- 6 files changed, 204 insertions(+), 149 deletions(-) delete mode 100644 assets/images/dm_tip/player_dm_tip_center.svg delete mode 100644 assets/images/dm_tip/player_dm_tip_left.svg delete mode 100644 assets/images/dm_tip/player_dm_tip_right.svg diff --git a/assets/images/dm_tip/player_dm_tip_center.svg b/assets/images/dm_tip/player_dm_tip_center.svg deleted file mode 100644 index 9c362584..00000000 --- a/assets/images/dm_tip/player_dm_tip_center.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/images/dm_tip/player_dm_tip_left.svg b/assets/images/dm_tip/player_dm_tip_left.svg deleted file mode 100644 index 1173549f..00000000 --- a/assets/images/dm_tip/player_dm_tip_left.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/images/dm_tip/player_dm_tip_right.svg b/assets/images/dm_tip/player_dm_tip_right.svg deleted file mode 100644 index 5d5260ce..00000000 --- a/assets/images/dm_tip/player_dm_tip_right.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 91857a78..a6fbc0c7 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -320,7 +320,7 @@ class PlPlayerController { } /// 弹幕权重 - late final enableTapDm = Utils.isMobile && Pref.enableTapDm; + late final enableTapDm = Pref.enableTapDm; late int danmakuWeight = Pref.danmakuWeight; late RuleFilter filters = Pref.danmakuFilterRule; // 关联弹幕控制器 diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index 4733b69c..e51fd492 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -63,7 +63,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:flutter_volume_controller/flutter_volume_controller.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart' hide ContextExtensionss; @@ -1139,24 +1138,22 @@ class _PLVideoPlayerState extends State } void _onTapDown(TapDownDetails details) { - if (Utils.isMobile) { - final ctr = plPlayerController.danmakuController; - if (ctr != null) { - final pos = details.localPosition; - final item = ctr.findSingleDanmaku(pos); - if (item == null) { - if (_suspendedDm != null) { - _removeDmAction(); - } - } else if (item != _suspendedDm) { - if (item.content.extra == null) { - _removeDmAction(); - return; - } - _suspendedDm?.suspend = false; - _suspendedDm = item..suspend = true; - _dmOffset = pos; + final ctr = plPlayerController.danmakuController; + if (ctr != null) { + final pos = details.localPosition; + final item = ctr.findSingleDanmaku(pos); + if (item == null) { + if (_suspendedDm != null) { + _removeDmAction(); } + } else if (item != _suspendedDm) { + if (item.content.extra == null) { + _removeDmAction(); + return; + } + _suspendedDm?.suspend = false; + _suspendedDm = item..suspend = true; + _dmOffset = pos; } } } @@ -2196,7 +2193,7 @@ class _PLVideoPlayerState extends State } static const _overlaySpacing = 10.0; - static const _overlayWidth = 118.0; + static const _overlayItemWidth = 40.0; static const _overlayHeight = 35.0; DanmakuItem? _suspendedDm; @@ -2219,7 +2216,7 @@ class _PLVideoPlayerState extends State }, child: SizedBox( height: _overlayHeight, - width: _overlayWidth / 3, + width: _overlayItemWidth, child: Center( child: child, ), @@ -2227,15 +2224,17 @@ class _PLVideoPlayerState extends State ); } - String _getDmTipBg(DanmakuItem item) { - const offset = 65; - if (item.xPosition >= maxWidth - offset) { - return 'right'; + static final _timeRegExp = RegExp(r'(?:\d+[::])?\d+[::][0-5]?\d(?!\d)'); + + int? _getValidOffset(String data) { + if (_timeRegExp.firstMatch(data) case final timeStr?) { + final offset = DurationUtils.parseDuration(timeStr.group(0)); + if (0 < offset && + offset * 1000 < videoDetailController.data.timeLength!) { + return offset; + } } - if (item.xPosition + item.width <= offset) { - return 'left'; - } - return 'center'; + return null; } Widget _buildDmAction( @@ -2249,17 +2248,24 @@ class _PLVideoPlayerState extends State return const Positioned(left: 0, top: 0, child: SizedBox.shrink()); } + final seekOffset = _getValidOffset(item.content.text); + + final overlayWidth = _overlayItemWidth * (seekOffset == null ? 3 : 4); + final dy = item.content.type == DanmakuItemType.bottom ? maxHeight - item.yPosition - item.height : item.yPosition; final top = dy + item.height + 4; - final right = - maxWidth - - clampDouble( - dx + _overlayWidth / 2, - _overlaySpacing + _overlayWidth, - maxWidth - _overlaySpacing, - ); + + final realLeft = dx + overlayWidth / 2; + + final left = realLeft.clamp( + _overlaySpacing + overlayWidth, + maxWidth - _overlaySpacing, + ); + + final right = maxWidth - left; + final triangleOffset = realLeft - left; if (right > (maxWidth - item.xPosition)) { _removeDmAction(); @@ -2271,116 +2277,112 @@ class _PLVideoPlayerState extends State return Positioned( right: right, top: top, - child: SizedBox( - width: _overlayWidth, - height: _overlayHeight, - child: Stack( - children: [ - SvgPicture.asset( - 'assets/images/dm_tip/player_dm_tip_${_getDmTipBg(item)}.svg', - clipBehavior: Clip.none, - width: _overlayWidth, - height: _overlayHeight, - ), - Positioned.fill( - top: 4, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: extra is VideoDanmaku - ? [ - _dmActionItem( - extra.isLike - ? const Icon( - size: 20, - CustomIcons.player_dm_tip_like_solid, - color: Colors.white, - ) - : const Icon( - size: 20, - CustomIcons.player_dm_tip_like, - color: Colors.white, - ), - onTap: () => HeaderControl.likeDanmaku( - extra, - plPlayerController.cid!, - ), - ), - _dmActionItem( - const Icon( - size: 19, - CustomIcons.player_dm_tip_copy, - color: Colors.white, - ), - onTap: () => Utils.copyText(item.content.text), - ), - if (item.content.selfSend) - _dmActionItem( - const Icon( - size: 20, - CustomIcons.player_dm_tip_recall, - color: Colors.white, - ), - onTap: () => HeaderControl.deleteDanmaku( - extra.id, - plPlayerController.cid!, - ), - ) - else - _dmActionItem( - const Icon( - size: 20, - CustomIcons.player_dm_tip_back, - color: Colors.white, - ), - onTap: () => HeaderControl.reportDanmaku( - context, - extra: extra, - ctr: plPlayerController, - ), - ), - ] - : extra is LiveDanmaku - ? [ - _dmActionItem( - const Icon( - size: 20, - MdiIcons.accountOutline, - color: Colors.white, - ), - onTap: () => Get.toNamed('/member?mid=${extra.mid}'), - ), - _dmActionItem( - const Icon( - size: 19, - CustomIcons.player_dm_tip_copy, - color: Colors.white, - ), - onTap: () => Utils.copyText(item.content.text), - ), - _dmActionItem( - const Icon( - size: 20, - CustomIcons.player_dm_tip_back, - color: Colors.white, - ), - onTap: () => HeaderControl.reportLiveDanmaku( - context, - roomId: - (widget.bottomControl - as live_bottom.BottomControl) - .liveRoomCtr - .roomId, - msg: item.content.text, - extra: extra, - ctr: plPlayerController, - ), - ), - ] - : throw UnimplementedError(), + child: CustomPaint( + painter: _DanmakuTipPainter(offset: triangleOffset), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: switch (extra) { + null => throw UnimplementedError(), + VideoDanmaku() => [ + _dmActionItem( + extra.isLike + ? const Icon( + size: 20, + CustomIcons.player_dm_tip_like_solid, + color: Colors.white, + ) + : const Icon( + size: 20, + CustomIcons.player_dm_tip_like, + color: Colors.white, + ), + onTap: () => HeaderControl.likeDanmaku( + extra, + plPlayerController.cid!, + ), ), - ), - ], + _dmActionItem( + const Icon( + size: 19, + CustomIcons.player_dm_tip_copy, + color: Colors.white, + ), + onTap: () => Utils.copyText(item.content.text), + ), + if (item.content.selfSend) + _dmActionItem( + const Icon( + size: 20, + CustomIcons.player_dm_tip_recall, + color: Colors.white, + ), + onTap: () => HeaderControl.deleteDanmaku( + extra.id, + plPlayerController.cid!, + ), + ) + else + _dmActionItem( + const Icon( + size: 20, + CustomIcons.player_dm_tip_back, + color: Colors.white, + ), + onTap: () => HeaderControl.reportDanmaku( + context, + extra: extra, + ctr: plPlayerController, + ), + ), + if (seekOffset != null) + _dmActionItem( + const Icon( + size: 20, + Icons.gps_fixed_outlined, + color: Colors.white, + ), + onTap: () => plPlayerController.seekTo( + Duration(seconds: seekOffset), + isSeek: false, + ), + ), + ], + LiveDanmaku() => [ + _dmActionItem( + const Icon( + size: 20, + MdiIcons.accountOutline, + color: Colors.white, + ), + onTap: () => Get.toNamed('/member?mid=${extra.mid}'), + ), + _dmActionItem( + const Icon( + size: 19, + CustomIcons.player_dm_tip_copy, + color: Colors.white, + ), + onTap: () => Utils.copyText(item.content.text), + ), + _dmActionItem( + const Icon( + size: 20, + CustomIcons.player_dm_tip_back, + color: Colors.white, + ), + onTap: () => HeaderControl.reportLiveDanmaku( + context, + roomId: (widget.bottomControl as live_bottom.BottomControl) + .liveRoomCtr + .roomId, + msg: item.content.text, + extra: extra, + ctr: plPlayerController, + ), + ), + ], + }, ), ), ); @@ -2729,3 +2731,59 @@ Widget buildViewPointWidget( ), ); } + +class _DanmakuTipPainter extends CustomPainter { + final double offset; + + const _DanmakuTipPainter({this.offset = 0}); + + @override + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = const Color(0xB3000000) + ..style = PaintingStyle.fill; + + final strokePaint = Paint() + ..color = const Color(0x7EFFFFFF) + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5; + + final radius = size.height / 2; + final triangleHeight = size.height / 6; + final triangleBase = triangleHeight * 2 / 3; + + final triangleCenterX = (size.width / 2 + offset).clamp( + radius + triangleBase, + size.width - radius - triangleBase, + ); + final path = Path() + // triangle (exceed) + ..moveTo(triangleCenterX - triangleBase, 0) + ..lineTo(triangleCenterX, -triangleHeight) + ..lineTo(triangleCenterX + triangleBase, 0) + // top + ..lineTo(size.width - radius, 0) + // right + ..arcToPoint( + Offset(size.width - radius, size.height), + radius: Radius.circular(radius), + ) + // bottom + ..lineTo(radius, size.height) + // left + ..arcToPoint( + Offset(radius, 0), + radius: Radius.circular(radius), + ) + ..close(); + + canvas + ..drawPath(path, paint) + ..drawPath(path, strokePaint); + } + + @override + bool shouldRepaint(covariant _DanmakuTipPainter oldDelegate) => + oldDelegate.offset != offset; +} diff --git a/lib/utils/storage_pref.dart b/lib/utils/storage_pref.dart index 5d11c93c..f233f6d0 100644 --- a/lib/utils/storage_pref.dart +++ b/lib/utils/storage_pref.dart @@ -860,7 +860,7 @@ abstract class Pref { _setting.get(SettingBoxKey.enablePlayAll, defaultValue: true); static bool get enableTapDm => - _setting.get(SettingBoxKey.enableTapDm, defaultValue: true); + _setting.get(SettingBoxKey.enableTapDm, defaultValue: Utils.isMobile); static bool get showTrayIcon => _setting.get(SettingBoxKey.showTrayIcon, defaultValue: true);