From b2fb4c9afe33fab6451efd50bf34fa9bfc2c6543 Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Thu, 16 Oct 2025 13:17:56 +0800 Subject: [PATCH] opt dm action Signed-off-by: bggRGjQaUbCoE --- assets/fonts/custom_icon.ttf | Bin 11420 -> 13416 bytes 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/common/widgets/custom_icon.dart | 31 ++-- .../widgets/image/flutter_svg_provider.dart | 159 ++++++++++++++++ lib/pages/setting/models/play_settings.dart | 1 + lib/plugin/pl_player/view.dart | 173 +++++++++--------- pubspec.yaml | 1 + 9 files changed, 272 insertions(+), 96 deletions(-) create mode 100644 assets/images/dm_tip/player_dm_tip_center.svg create mode 100644 assets/images/dm_tip/player_dm_tip_left.svg create mode 100644 assets/images/dm_tip/player_dm_tip_right.svg create mode 100644 lib/common/widgets/image/flutter_svg_provider.dart diff --git a/assets/fonts/custom_icon.ttf b/assets/fonts/custom_icon.ttf index ea091ac596834b71780fd8cc0b18696d4b1a15ce..88e7f64fce7f3d4d6f87f8313101725356c73677 100644 GIT binary patch delta 2420 zcmZ8hYit}>6~5=r+&gpUy^opMhj-UIYwxb@*c(4~H24oOI8T9uYIfda;M432Ck zu5F|QXcm#w;w9SPCK76;^hc=#KOj{?Do`a#lz*U7R8T4!6e<-k2>K%u^iSDvW;QKq z=gxhdd(OG{obR0T?5DRDo+`DTBm}-o2zg{~d1mGB?@iq%gdW2F%9oZ-eepLt_CHMs z+e=9Qk%jr0ql2U0cmsL=jJ>gd1ZAhRjO&-N_be>0pPYXRR|MY1VPWaS+{|-V&i@4E z4cs@DXHKp_9WLW}3isW|XO`zT-pVWJ{y*P&OxltH+^d9!RQy7@%w`^5gw*D1u<(7F+}uGE zeP^QsLQWGYT5ZEwL+-OgCJ&;Bj15J%Qk(1a9@8Zr@kxrLk%M1;ky3C-Xbgd!;fP;) z^O|(KwUtB>?8L?SZbwnFb+@AuvURtkI#l23pmnmw({AdQjKH{H zylsBX>bHCBOHSat6Pt})i*F>3B-h<-D1&z1xh9>X?~($!{4cp0Z^=mchx9wiCV6s* z93`vxObZlF6bt-36u|Sn(PDEFnvHU$Oe#a*dlB)2DF+@$W`!%f*o)`&v1VfzG@4QK zqLX9+@<)~r_4m_#Kmd0CA!00|pq zCEpW{99NyJAu_fusdBg9GTIf#9dw89btpp~>tnU_l<&anYBAVP6-m*p?WNO)Rtwu4 z!#gn{(NzZ$6U`L`soiQ;KOy@%}Z#2EmM^T~8mZvP7c&9epst1kiXs zA18#Pw|^iVrhgyidL(pFvr!Mdgp1y0qf$n11tsoVU<3MC zaYJXm4~;)E4kL{ao{r!y7>fjO2ucA90d)OR5CLRC4oWpB*B};h4~GD?ip&Zj_D902 z$MCFFRsugz0)>ZUZzOuoSPfCC_;`IV(RtYr3IMX`P4hGql^Byv zdEl0x{$zf;0djQ4Y(wa}Y%03v>_{IDG_zZ8S?9TcIsu$iA=~4^tM${8#_=eL;HAf9I-gxOhdMs@Sw25Q( z0+k(MTo7JCO+Rrun|Hbg(bzpKrs)?iNj`Cg1KPVw6}GW(XpWYd73` zh<@isa5~UOIfbB?OXX5fj)Gwf;|6~OF%PaKMM+zL(P$s4ivAW(HChk#NX>=^ZYAi5 zg|K&GYLxcpdYwd&$$4tLuMz7nYKjuM22+(=Bi-&_Kl`mrFp)F|j989B);CqnON>pX zFiS#M2J@nnw+$|X@Ia-EshLl$amF!?Xe(vvWm7B0ofjT`0F_5&i#B({kcNAs&g7% zq4t}JLAx@p$*@=Ttz_IA+?^D(6O%1TFvaS9yubTl*G|b8cJQa0lBnj!@6V&UcJCV) zK7rr6IXo0g2+KvOu=a*^^i^qt-X=rj;~xMOVq-&WG)l#Ah{_=56yj1T4Rvx9-5VCz z;V6}pEks8`NJe7=z~NoY8&#o7S6b-W^XuD0Pu4F9+Kr7CGE`GU>fr-j7-^JOV#Dp{ z-Lk-<6$Yr`y0)s%))Qs=Ptcff-R36>x={c77M|*HrQi$qfK{% zG-4X>l!-8gDnqnV9*F8%vw_z4sV_;K(<`c;;VsVPiAi|6Vrw?7?RPZLVh`P844c8G0xO13e=c8Dbdz0`71h8PLtU73;OrkN z94yoiTIih6JuzLiLq|zYZQ^tBUGecROeoS++D|fSqy)K~^dCz?W^-epUjrXfkcJFo zH!ls0wc;yFGpFWPr;jdAuOC~Po}HOn{0M38#LB6Ukd}@u&Zj;s7?P&fPAnZen*8v= d>ipcy(h^%+m|30IqH%iZ*zxz5o9_)={2xuKe~AD9 delta 390 zcmaEnF(-0@a(x#A17i;Z14CGHZeoG<&9~bb7?>=8xGFuTGVOQX#rq5lEDsnMloK*i z6H|0F?o9!z1@alpGJpc?Hq047_8uTs$;d6K2;u;;7#N=biIAN9zqsV4=G<}y2JIA}BGZX`lwT8Gq@PI)^V@kG2vOpTg3Z}&yAmp|Bk>9 h!3~>TSWSgC7s?fgFp5roWTZ7&%-Ce|M&s>_3;?!!S| \ 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 new file mode 100644 index 00000000..1173549f --- /dev/null +++ b/assets/images/dm_tip/player_dm_tip_left.svg @@ -0,0 +1 @@ + \ 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 new file mode 100644 index 00000000..5d5260ce --- /dev/null +++ b/assets/images/dm_tip/player_dm_tip_right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/common/widgets/custom_icon.dart b/lib/common/widgets/custom_icon.dart index 8555a8d3..69d1c893 100644 --- a/lib/common/widgets/custom_icon.dart +++ b/lib/common/widgets/custom_icon.dart @@ -10,19 +10,24 @@ class CustomIcons { static const IconData dyn = _CustomIconData(0xe804); static const IconData fav = _CustomIconData(0xe805); static const IconData live_reserve = _CustomIconData(0xe806); - static const IconData share = _CustomIconData(0xe807); - static const IconData share_line = _CustomIconData(0xe808); - static const IconData share_node = _CustomIconData(0xe809); - static const IconData star_favorite_line = _CustomIconData(0xe80a); - static const IconData star_favorite_solid = _CustomIconData(0xe80b); - static const IconData thumbs_down = _CustomIconData(0xe80c); - static const IconData thumbs_down_outline = _CustomIconData(0xe80d); - static const IconData thumbs_up = _CustomIconData(0xe80e); - static const IconData thumbs_up_fill = _CustomIconData(0xe80f); - static const IconData thumbs_up_line = _CustomIconData(0xe810); - static const IconData thumbs_up_outline = _CustomIconData(0xe811); - static const IconData topic_tag = _CustomIconData(0xe812); - static const IconData watch_later = _CustomIconData(0xe813); + static const IconData player_dm_tip_back = _CustomIconData(0xe807); + static const IconData player_dm_tip_copy = _CustomIconData(0xe808); + static const IconData player_dm_tip_like = _CustomIconData(0xe809); + static const IconData player_dm_tip_like_solid = _CustomIconData(0xe80a); + static const IconData player_dm_tip_recall = _CustomIconData(0xe80b); + static const IconData share = _CustomIconData(0xe80c); + static const IconData share_line = _CustomIconData(0xe80d); + static const IconData share_node = _CustomIconData(0xe80e); + static const IconData star_favorite_line = _CustomIconData(0xe80f); + static const IconData star_favorite_solid = _CustomIconData(0xe810); + static const IconData thumbs_down = _CustomIconData(0xe811); + static const IconData thumbs_down_outline = _CustomIconData(0xe812); + static const IconData thumbs_up = _CustomIconData(0xe813); + static const IconData thumbs_up_fill = _CustomIconData(0xe814); + static const IconData thumbs_up_line = _CustomIconData(0xe815); + static const IconData thumbs_up_outline = _CustomIconData(0xe816); + static const IconData topic_tag = _CustomIconData(0xe817); + static const IconData watch_later = _CustomIconData(0xe818); } class _CustomIconData extends IconData { diff --git a/lib/common/widgets/image/flutter_svg_provider.dart b/lib/common/widgets/image/flutter_svg_provider.dart new file mode 100644 index 00000000..dabe06cd --- /dev/null +++ b/lib/common/widgets/image/flutter_svg_provider.dart @@ -0,0 +1,159 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:flutter_svg/flutter_svg.dart'; + +/// https://github.com/yang-f/flutter_svg_provider + +/// Rasterizes given svg picture for displaying in [Image] widget: +/// +/// ```dart +/// Image( +/// width: 32, +/// height: 32, +/// image: Svg('assets/my_icon.svg'), +/// ) +/// ``` +class SvgImageProvider extends ImageProvider { + /// Path to svg file or asset + final String path; + + /// Size in logical pixels to render. + /// Useful for [DecorationImage]. + /// If not specified, will use size from [Image]. + /// If [Image] not specifies size too, will use default size 100x100. + final Size? size; + + /// Color to tint the SVG + final Color? color; + + /// Image scale. + final double? scale; + + /// Width and height can also be specified from [Image] constructor. + /// Default size is 100x100 logical pixels. + /// Different size can be specified in [Image] parameters + const SvgImageProvider( + this.path, { + this.size, + this.scale, + this.color, + }); + + @override + Future obtainKey(ImageConfiguration configuration) { + final Color color = this.color ?? Colors.transparent; + final double scale = this.scale ?? configuration.devicePixelRatio ?? 1.0; + final double logicWidth = size?.width ?? configuration.size?.width ?? 100; + final double logicHeight = + size?.height ?? configuration.size?.height ?? 100; + + return SynchronousFuture( + SvgImageKey( + path: path, + scale: scale, + color: color, + pixelWidth: (logicWidth * scale).round(), + pixelHeight: (logicHeight * scale).round(), + ), + ); + } + + @override + ImageStreamCompleter loadImage(SvgImageKey key, ImageDecoderCallback decode) { + return OneFrameImageStreamCompleter( + _loadAsync(key, getFilterColor(color)), + ); + } + + static Future _loadAsync(SvgImageKey key, Color color) async { + final rawSvg = await rootBundle.loadString(key.path); + final pictureInfo = await vg.loadPicture( + SvgStringLoader(rawSvg, theme: SvgTheme(currentColor: color)), + null, + clipViewbox: false, + ); + + try { + final image = pictureInfo.picture.toImageSync( + pictureInfo.size.width.round(), + pictureInfo.size.height.round(), + ); + return ImageInfo(image: image); + } finally { + // Dispose of the Picture to release resources + pictureInfo.picture.dispose(); + } + } + + // Note: == and hashCode not overrided as changes in properties + // (width, height and scale) are not observable from the here. + // [SvgImageKey] instances will be compared instead. + @override + String toString() => '$runtimeType(${describeIdentity(path)})'; + + // Running on web with Colors.transparent may throws the exception `Expected a value of type 'SkDeletable', but got one of type 'Null'`. + static Color getFilterColor(Color? color) { + if (kIsWeb && color == Colors.transparent) { + return const Color(0x01ffffff); + } else { + return color ?? Colors.transparent; + } + } +} + +@immutable +class SvgImageKey { + const SvgImageKey({ + required this.path, + required this.pixelWidth, + required this.pixelHeight, + required this.scale, + this.color, + }); + + /// Path to svg asset. + final String path; + + /// Width in physical pixels. + /// Used when raterizing. + final int pixelWidth; + + /// Height in physical pixels. + /// Used when raterizing. + final int pixelHeight; + + /// Color to tint the SVG + final Color? color; + + /// Used to calculate logical size from physical, i.e. + /// logicalWidth = [pixelWidth] / [scale], + /// logicalHeight = [pixelHeight] / [scale]. + /// Should be equal to [MediaQueryData.devicePixelRatio]. + final double scale; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + + return other is SvgImageKey && + other.path == path && + other.pixelWidth == pixelWidth && + other.pixelHeight == pixelHeight && + other.scale == scale && + other.color == color; + } + + @override + int get hashCode => Object.hash( + path, + pixelWidth, + pixelHeight, + scale, + color, + ); +} diff --git a/lib/pages/setting/models/play_settings.dart b/lib/pages/setting/models/play_settings.dart index 6eff199f..34e07caf 100644 --- a/lib/pages/setting/models/play_settings.dart +++ b/lib/pages/setting/models/play_settings.dart @@ -33,6 +33,7 @@ List get playSettings => [ const SettingsModel( settingsType: SettingsType.sw1tch, title: '启用点击弹幕', + subtitle: '点击弹幕悬停,支持点赞、复制、举报操作', leading: Icon(Icons.touch_app_outlined), setKey: SettingBoxKey.enableTapDm, defaultVal: true, diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index 1e846729..065a8847 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -4,8 +4,10 @@ import 'dart:math' as math; import 'dart:ui' as ui; import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/gesture/immediate_tap_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/gesture/mouse_interactive_viewer.dart'; +import 'package:PiliPlus/common/widgets/image/flutter_svg_provider.dart'; import 'package:PiliPlus/common/widgets/loading_widget.dart'; import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/common/widgets/progress_bar/audio_video_progress_bar.dart'; @@ -867,14 +869,15 @@ class _PLVideoPlayerState extends State void _onInteractionStart(ScaleStartDetails details) { if (plPlayerController.controlsLock.value) return; // 如果起点太靠上则屏蔽 - if (details.localFocalPoint.dy < 40) return; - if (details.localFocalPoint.dx < 40) return; - if (details.localFocalPoint.dx > maxWidth - 40) return; - if (details.localFocalPoint.dy > maxHeight - 40) return; + final localFocalPoint = details.localFocalPoint; + final dx = localFocalPoint.dx; + final dy = localFocalPoint.dy; + if (dx < 40 || dy < 40) return; + if (dx > maxWidth - 40 || dy > maxHeight - 40) return; if (details.pointerCount > 1) { interacting = true; } - plPlayerController.initialFocalPoint = details.localFocalPoint; + plPlayerController.initialFocalPoint = localFocalPoint; // if (kDebugMode) { // debugPrint("_initialFocalPoint$_initialFocalPoint"); // } @@ -899,10 +902,12 @@ class _PLVideoPlayerState extends State if (_gestureType == null) { if (cumulativeDelta.distance < 1) return; - if (cumulativeDelta.dx.abs() > 3 * cumulativeDelta.dy.abs()) { + final dx = cumulativeDelta.dx.abs(); + final dy = cumulativeDelta.dy.abs(); + if (dx > 3 * dy) { _gestureType = GestureType.horizontal; _showControlsIfNeeded(); - } else if (cumulativeDelta.dy.abs() > 3 * cumulativeDelta.dx.abs()) { + } else if (dy > 3 * dx) { if (!plPlayerController.enableSlideVolumeBrightness && !plPlayerController.enableSlideFS) { return; @@ -1208,10 +1213,12 @@ class _PLVideoPlayerState extends State if (_gestureType == null) { final pan = event.pan; if (pan.distance < 1) return; - if (pan.dx.abs() > 3 * pan.dy.abs()) { + final dx = pan.dx.abs(); + final dy = pan.dy.abs(); + if (dx > 3 * dy) { _gestureType = GestureType.horizontal; _showControlsIfNeeded(); - } else if (pan.dy.abs() > 3 * pan.dx.abs()) { + } else if (dy > 3 * dx) { _gestureType = GestureType.right; } return; @@ -2179,7 +2186,7 @@ class _PLVideoPlayerState extends State } static const _overlaySpacing = 10.0; - static const _overlayWidth = 130.0; + static const _overlayWidth = 118.0; static const _overlayHeight = 35.0; DanmakuItem? _suspendedDm; @@ -2196,7 +2203,10 @@ class _PLVideoPlayerState extends State Widget _dmActionItem(Widget child, {required VoidCallback onTap}) { return GestureDetector( behavior: HitTestBehavior.opaque, - onTap: onTap, + onTap: () { + _removeDmAction(); + onTap(); + }, child: SizedBox( height: _overlayHeight, width: _overlayWidth / 3, @@ -2207,12 +2217,49 @@ class _PLVideoPlayerState extends State ); } + BoxDecoration _getDmTipBg(DanmakuItem item, double dx) { + const offset = 65; + const size = Size(_overlayWidth, _overlayHeight); + if (item.xPosition >= maxWidth - offset) { + return const BoxDecoration( + image: DecorationImage( + filterQuality: FilterQuality.low, + image: SvgImageProvider( + 'assets/images/dm_tip/player_dm_tip_right.svg', + size: size, + ), + ), + ); + } + if (item.xPosition + item.width <= offset) { + return const BoxDecoration( + image: DecorationImage( + filterQuality: FilterQuality.low, + image: SvgImageProvider( + 'assets/images/dm_tip/player_dm_tip_left.svg', + size: size, + ), + ), + ); + } + return const BoxDecoration( + image: DecorationImage( + filterQuality: FilterQuality.low, + image: SvgImageProvider( + 'assets/images/dm_tip/player_dm_tip_center.svg', + size: size, + ), + ), + ); + } + Widget _buildDmAction( DanmakuItem item, Offset offset, ) { + final dx = offset.dx; // fullscreen - if (offset.dx > maxWidth) { + if (dx > maxWidth) { _removeDmAction(); return const Positioned(left: 0, top: 0, child: SizedBox.shrink()); } @@ -2224,28 +2271,24 @@ class _PLVideoPlayerState extends State final right = maxWidth - clampDouble( - offset.dx + _overlayWidth / 2, + dx + _overlayWidth / 2, _overlaySpacing + _overlayWidth, maxWidth - _overlaySpacing, ); + // TODO LiveDanmaku final extra = item.content.extra as VideoDanmaku; + return Positioned( right: right, top: top, - child: Column( - children: [ - const CustomPaint( - painter: _TrianglePainter(Colors.black54), - size: Size(12, 6), - ), - Container( - width: _overlayWidth, - height: _overlayHeight, - decoration: const BoxDecoration( - color: Colors.black54, - borderRadius: BorderRadius.all(Radius.circular(18)), - ), + child: SizedBox( + width: _overlayWidth, + height: _overlayHeight, + child: DecoratedBox( + decoration: _getDmTipBg(item, dx), + child: Padding( + padding: const EdgeInsets.only(top: 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -2254,64 +2297,52 @@ class _PLVideoPlayerState extends State Icon( size: 20, extra.isLike - ? Icons.thumb_up_off_alt_sharp - : Icons.thumb_up_off_alt_outlined, + ? CustomIcons.player_dm_tip_like_solid + : CustomIcons.player_dm_tip_like, color: Colors.white, ), - onTap: () { - _removeDmAction(); - HeaderControl.likeDanmaku( - extra, - plPlayerController.cid!, - ); - }, + onTap: () => HeaderControl.likeDanmaku( + extra, + plPlayerController.cid!, + ), ), _dmActionItem( const Icon( - size: 20, - Icons.copy, + size: 19, + CustomIcons.player_dm_tip_copy, color: Colors.white, ), - onTap: () { - _removeDmAction(); - Utils.copyText(item.content.text); - }, + onTap: () => Utils.copyText(item.content.text), ), if (item.content.selfSend) _dmActionItem( const Icon( size: 20, - Icons.delete, + CustomIcons.player_dm_tip_recall, color: Colors.white, ), - onTap: () { - _removeDmAction(); - HeaderControl.deleteDanmaku( - extra.id, - plPlayerController.cid!, - ); - }, + onTap: () => HeaderControl.deleteDanmaku( + extra.id, + plPlayerController.cid!, + ), ) else _dmActionItem( const Icon( size: 20, - Icons.report_problem_outlined, + CustomIcons.player_dm_tip_back, color: Colors.white, ), - onTap: () { - _removeDmAction(); - HeaderControl.reportDanmaku( - extra, - context, - plPlayerController, - ); - }, + onTap: () => HeaderControl.reportDanmaku( + extra, + context, + plPlayerController, + ), ), ], ), ), - ], + ), ), ); } @@ -2659,27 +2690,3 @@ Widget buildViewPointWidget( ), ); } - -class _TrianglePainter extends CustomPainter { - const _TrianglePainter(this.color); - final Color color; - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = color - ..style = PaintingStyle.fill; - - final path = Path() - ..moveTo(0, size.height) - ..lineTo(size.width, size.height) - ..lineTo(size.width / 2, 0) - ..close(); - - canvas.drawPath(path, paint); - } - - @override - bool shouldRepaint(covariant _TrianglePainter oldDelegate) => - color != oldDelegate.color; -} diff --git a/pubspec.yaml b/pubspec.yaml index 68fbc6f9..a6284aad 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -326,6 +326,7 @@ flutter: - assets/images/live/ - assets/images/video/ - assets/images/paycoins/ + - assets/images/dm_tip/ - assets/shaders/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware