diff --git a/lib/pages/video/widgets/header_control.dart b/lib/pages/video/widgets/header_control.dart index e04a3552e..d39096744 100644 --- a/lib/pages/video/widgets/header_control.dart +++ b/lib/pages/video/widgets/header_control.dart @@ -65,6 +65,76 @@ class HeaderControl extends StatefulWidget { @override State createState() => HeaderControlState(); + + static Future likeDanmaku(VideoDanmaku extra, int cid) async { + if (!Accounts.main.isLogin) { + SmartDialog.showToast('请先登录'); + return false; + } + final res = await DanmakuHttp.danmakuLike( + isLike: extra.isLike, + cid: cid, + id: extra.id, + ); + if (res.isSuccess) { + extra.isLike = !extra.isLike; + return true; + } else { + res.toast(); + return false; + } + } + + static Future deleteDanmaku(int id, int cid) async { + final res = await DanmakuHttp.danmakuRecall( + cid: cid, + id: id, + ); + if (res.isSuccess) { + SmartDialog.showToast('删除成功'); + return true; + } else { + res.toast(); + return false; + } + } + + static Future reportDanmaku( + VideoDanmaku extra, + BuildContext context, + PlPlayerController ctr, + ) { + if (Accounts.main.isLogin) { + return autoWrapReportDialog( + context, + ReportOptions.danmakuReport, + (reasonType, reasonDesc, banUid) { + if (banUid) { + final filter = ctr.filters; + if (filter.dmUid.add(extra.mid)) { + filter.count++; + GStorage.localCache.put( + LocalCacheKey.danmakuFilterRules, + filter, + ); + } + DanmakuFilterHttp.danmakuFilterAdd( + filter: extra.mid, + type: 2, + ); + } + return DanmakuHttp.danmakuReport( + reason: reasonType == 0 ? 11 : reasonType, + cid: ctr.cid!, + id: extra.id, + content: reasonDesc, + ); + }, + ); + } else { + return SmartDialog.showToast('请先登录'); + } + } } class HeaderControlState extends State { @@ -1868,34 +1938,34 @@ class HeaderControlState extends State { clipBehavior: Clip.hardEdge, color: theme.colorScheme.surface, borderRadius: const BorderRadius.all(Radius.circular(12)), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 14), - child: CustomScrollView( - slivers: [ - SliverPersistentHeader( - pinned: true, - delegate: CustomSliverPersistentHeaderDelegate( - child: Padding( - padding: const EdgeInsets.all(6), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('弹幕列表'), - IconButton( - onPressed: () => setState(() {}), - icon: const Icon(Icons.refresh), - ), - ], - ), + child: CustomScrollView( + slivers: [ + SliverPersistentHeader( + pinned: true, + delegate: CustomSliverPersistentHeaderDelegate( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 7, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('弹幕列表'), + IconButton( + onPressed: () => setState(() {}), + icon: const Icon(Icons.refresh), + ), + ], ), - bgColor: theme.colorScheme.surface, ), + bgColor: theme.colorScheme.surface, ), - ?_buildDanmakuList(ctr.staticDanmaku), - ?_buildDanmakuList(ctr.scrollDanmaku), - ?_buildDanmakuList(ctr.specialDanmaku), - ], - ), + ), + ?_buildDanmakuList(ctr.staticDanmaku), + ?_buildDanmakuList(ctr.scrollDanmaku), + ?_buildDanmakuList(ctr.specialDanmaku), + ], ), ), ); @@ -1913,83 +1983,43 @@ class HeaderControlState extends State { final extra = item.content.extra! as VideoDanmaku; return ListTile( dense: true, - contentPadding: const EdgeInsets.only(left: 6), + contentPadding: const EdgeInsets.symmetric(horizontal: 14), onLongPress: () => Utils.copyText(item.content.text), - title: Text(item.content.text * 10), + title: Text(item.content.text), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ Builder( builder: (context) => IconButton( onPressed: () async { - if (!Accounts.main.isLogin) { - SmartDialog.showToast('请先登录'); - return; - } - final res = await DanmakuHttp.danmakuLike( - isLike: extra.isLike, - cid: plPlayerController.cid!, - id: extra.id, - ); - if (res.isSuccess) { - extra.isLike = !extra.isLike; - if (context.mounted) { - (context as Element).markNeedsBuild(); - } - } else { - res.toast(); + if (await HeaderControl.likeDanmaku( + extra, + plPlayerController.cid!, + ) && + context.mounted) { + (context as Element).markNeedsBuild(); } }, icon: extra.isLike - ? const Icon(Icons.thumb_up) - : const Icon(Icons.thumb_up_outlined), + ? const Icon(Icons.thumb_up_off_alt_sharp) + : const Icon(Icons.thumb_up_off_alt_rounded), ), ), if (item.content.selfSend) IconButton( - onPressed: () async { - final res = await DanmakuHttp.danmakuRecall( - cid: plPlayerController.cid!, - id: extra.id, - ); - if (res.isSuccess) { - SmartDialog.showToast('删除成功'); - } else { - res.toast(); - } - }, + onPressed: () => HeaderControl.deleteDanmaku( + extra.id, + plPlayerController.cid!, + ), icon: const Icon(Icons.delete_outline), ) else IconButton( - onPressed: () { - autoWrapReportDialog( - context, - ReportOptions.danmakuReport, - (reasonType, reasonDesc, banUid) { - if (banUid) { - final filter = plPlayerController.filters; - if (filter.dmUid.add(extra.mid)) { - filter.count++; - GStorage.localCache.put( - LocalCacheKey.danmakuFilterRules, - filter, - ); - } - DanmakuFilterHttp.danmakuFilterAdd( - filter: extra.mid, - type: 2, - ); - } - return DanmakuHttp.danmakuReport( - reason: reasonType == 0 ? 11 : reasonType, - cid: plPlayerController.cid!, - id: extra.id, - content: reasonDesc, - ); - }, - ); - }, + onPressed: () => HeaderControl.reportDanmaku( + extra, + context, + plPlayerController, + ), icon: const Icon(Icons.report_problem_outlined), ), ], diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index 2d9483487..3fa1c49d8 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -21,10 +21,12 @@ import 'package:PiliPlus/models_new/video/video_detail/section.dart'; import 'package:PiliPlus/models_new/video/video_detail/ugc_season.dart'; import 'package:PiliPlus/models_new/video/video_shot/data.dart'; import 'package:PiliPlus/pages/common/common_intro_controller.dart'; +import 'package:PiliPlus/pages/danmaku/dnamaku_model.dart'; import 'package:PiliPlus/pages/video/controller.dart'; import 'package:PiliPlus/pages/video/introduction/pgc/controller.dart'; import 'package:PiliPlus/pages/video/post_panel/popup_menu_text.dart'; import 'package:PiliPlus/pages/video/post_panel/view.dart'; +import 'package:PiliPlus/pages/video/widgets/header_control.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/models/bottom_control_type.dart'; import 'package:PiliPlus/plugin/pl_player/models/bottom_progress_behavior.dart'; @@ -46,6 +48,7 @@ import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/utils.dart'; +import 'package:canvas_danmaku/canvas_danmaku.dart'; import 'package:dio/dio.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:fl_chart/fl_chart.dart'; @@ -1051,12 +1054,27 @@ class _PLVideoPlayerState extends State plPlayerController.triggerFullScreen(status: !isFullScreen); } - void onTap(PointerDeviceKind? kind) { - switch (kind) { - case ui.PointerDeviceKind.mouse when Utils.isDesktop: + void onTapUp(TapDownDetails? event) { + switch (event?.kind) { + case ui.PointerDeviceKind.mouse when (!kDebugMode && Utils.isDesktop): onTapDesktop(); break; default: + if (kDebugMode || Utils.isMobile) { + final ctr = plPlayerController.danmakuController; + if (ctr != null) { + final item = ctr.findSingleDanmaku(event!.globalPosition); + if (item == null) { + if (_suspendedDM != null) { + _removeOverlay(); + break; + } + } else if (item != _suspendedDM) { + _showOverlay(item, event, ctr); + break; + } + } + } plPlayerController.controls = !plPlayerController.showControls.value; break; } @@ -1175,8 +1193,6 @@ class _PLVideoPlayerState extends State } } - final isMobile = Utils.isMobile; - @override Widget build(BuildContext context) { maxWidth = widget.maxWidth; @@ -1223,12 +1239,12 @@ class _PLVideoPlayerState extends State !Utils.isDesktop && !plPlayerController.controlsLock.value, enableShrinkVideoSize: !Utils.isDesktop && plPlayerController.enableShrinkVideoSize, - onInteractionStart: _onInteractionStart, + onInteractionStart: _onInteractionStart, // TODO: refa gesture onInteractionUpdate: _onInteractionUpdate, onInteractionEnd: _onInteractionEnd, flipX: plPlayerController.flipX.value, flipY: plPlayerController.flipY.value, - onTap: onTap, + onTap: onTapUp, onDoubleTapDown: onDoubleTapDown, onLongPressStart: isLive ? null @@ -1656,7 +1672,7 @@ class _PLVideoPlayerState extends State ), ), ), - if (isMobile) + if (Utils.isMobile) buildViewPointWidget( videoDetailController, plPlayerController, @@ -1878,7 +1894,7 @@ class _PLVideoPlayerState extends State }), ], ); - if (!isMobile) { + if (!kDebugMode && !Utils.isMobile) { return Listener( behavior: HitTestBehavior.translucent, onPointerDown: onPointerDown, @@ -2045,6 +2061,154 @@ class _PLVideoPlayerState extends State }, ); } + + static const overlaySpacing = 10.0; + static const overlayWidth = 130.0; + static const overlayHeight = 35.0; + + DanmakuItem? _suspendedDM; + OverlayEntry? _overlayEntry; + + @override + void deactivate() { + _removeOverlay(); + super.deactivate(); + } + + void _removeOverlay() { + _suspendedDM?.suspend = false; + _suspendedDM = null; + _overlayEntry?.remove(); + _overlayEntry = null; + } + + Widget _overlayItem(Widget child, {required VoidCallback onTap}) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTap, + child: SizedBox( + height: overlayHeight, + width: overlayWidth / 3, + child: Center( + child: child, + ), + ), + ); + } + + void _showOverlay( + DanmakuItem item, + PositionedGestureDetails event, + DanmakuController ctr, + ) { + _removeOverlay(); + item.suspend = true; + _suspendedDM = item; + + final dy = item.content.type == DanmakuItemType.bottom + ? ctr.viewHeight - item.yPosition - item.height + : item.yPosition; + final extra = item.content.extra as VideoDanmaku; + + final theme = Theme.of(context); + + Overlay.of(context).insert( + _overlayEntry = OverlayEntry( + builder: (context) { + return Positioned( + top: dy + item.height + 4, + left: clampDouble( + event.globalPosition.dx - overlayWidth / 2, + overlaySpacing, + ctr.viewWidth - overlayWidth - overlaySpacing, + ), + child: Column( + children: [ + CustomPaint( + painter: _TrianglePainter( + theme.colorScheme.onSurface.withValues(alpha: 0.8), + ), + size: const Size(12, 6), + ), + Container( + width: overlayWidth, + height: overlayHeight, + decoration: BoxDecoration( + color: theme.colorScheme.onSurface.withValues(alpha: 0.8), + borderRadius: const BorderRadius.all(Radius.circular(18)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _overlayItem( + Icon( + size: 20, + extra.isLike + ? Icons.thumb_up_off_alt_sharp + : Icons.thumb_up_off_alt_outlined, + color: theme.colorScheme.surface, + ), + onTap: () { + _removeOverlay(); + HeaderControl.likeDanmaku( + extra, + plPlayerController.cid!, + ); + }, + ), + _overlayItem( + Icon( + size: 20, + Icons.copy, + color: theme.colorScheme.surface, + ), + onTap: () { + _removeOverlay(); + Utils.copyText(item.content.text); + }, + ), + if (item.content.selfSend) + _overlayItem( + Icon( + size: 20, + Icons.delete, + color: theme.colorScheme.surface, + ), + onTap: () { + _removeOverlay(); + HeaderControl.deleteDanmaku( + extra.id, + plPlayerController.cid!, + ); + }, + ) + else + _overlayItem( + Icon( + size: 20, + Icons.report_problem_outlined, + color: theme.colorScheme.surface, + ), + onTap: () { + _removeOverlay(); + HeaderControl.reportDanmaku( + extra, + context, + plPlayerController, + ); + }, + ), + ], + ), + ), + ], + ), + ); + }, + ), + ); + } } Widget buildDmChart( @@ -2389,3 +2553,27 @@ 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; +}