feat: tap danmaku (#1534)

This commit is contained in:
My-Responsitories
2025-10-12 21:51:59 +08:00
committed by GitHub
parent 35b34cb2d4
commit f6ca007815
2 changed files with 311 additions and 93 deletions

View File

@@ -65,6 +65,76 @@ class HeaderControl extends StatefulWidget {
@override
State<HeaderControl> createState() => HeaderControlState();
static Future<bool> 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<bool> 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<void> 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<HeaderControl> {
@@ -1868,34 +1938,34 @@ class HeaderControlState extends State<HeaderControl> {
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<HeaderControl> {
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),
),
],

View File

@@ -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<PLVideoPlayer>
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<PLVideoPlayer>
}
}
final isMobile = Utils.isMobile;
@override
Widget build(BuildContext context) {
maxWidth = widget.maxWidth;
@@ -1223,12 +1239,12 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
!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<PLVideoPlayer>
),
),
),
if (isMobile)
if (Utils.isMobile)
buildViewPointWidget(
videoDetailController,
plPlayerController,
@@ -1878,7 +1894,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
}),
],
);
if (!isMobile) {
if (!kDebugMode && !Utils.isMobile) {
return Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: onPointerDown,
@@ -2045,6 +2061,154 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
},
);
}
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<DanmakuExtra> item,
PositionedGestureDetails event,
DanmakuController<DanmakuExtra> 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;
}