report sc

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2026-01-03 15:44:37 +08:00
parent 2b5f111fb1
commit fd06fa9cc4
19 changed files with 678 additions and 397 deletions

View File

@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'package:PiliPlus/common/widgets/dialog/report.dart';
import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart';
import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/http/live.dart';
@@ -98,10 +99,11 @@ class LiveRoomController extends GetxController {
// dm
LiveDmInfoData? dmInfo;
List<RichTextItem>? savedDanmaku;
RxList<DanmakuMsg> messages = <DanmakuMsg>[].obs;
RxList<dynamic> messages = <dynamic>[].obs;
late final Rx<SuperChatItem?> fsSC = Rx<SuperChatItem?>(null);
late final RxList<SuperChatItem> superChatMsg = <SuperChatItem>[].obs;
RxBool disableAutoScroll = false.obs;
bool autoScroll = true;
LiveMessageStream? _msgStream;
late final ScrollController scrollController;
late final RxInt pageIndex = 0.obs;
@@ -297,6 +299,16 @@ class LiveRoomController extends GetxController {
}
void scrollToBottom([_]) {
EasyThrottle.throttle(
'liveDm',
const Duration(milliseconds: 500),
() => WidgetsBinding.instance.addPostFrameCallback(
_scrollToBottom,
),
);
}
void _scrollToBottom([_]) {
if (scrollController.hasClients) {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
@@ -326,7 +338,7 @@ class LiveRoomController extends GetxController {
messages.addAll(
list.cast<Map<String, dynamic>>().map(DanmakuMsg.fromPrefetch),
);
WidgetsBinding.instance.addPostFrameCallback(scrollToBottom);
scrollToBottom();
} catch (_) {}
}
}
@@ -424,6 +436,19 @@ class LiveRoomController extends GetxController {
..init();
}
void addDm(dynamic msg, [DanmakuContentItem<DanmakuExtra>? item]) {
messages.add(msg);
if (plPlayerController.showDanmaku) {
if (item != null) {
danmakuController?.addDanmaku(item);
}
if (autoScroll && !disableAutoScroll.value) {
scrollToBottom();
}
}
}
@pragma('vm:notify-debugger-on-exception')
void _danmakuListener(dynamic obj) {
try {
@@ -459,7 +484,7 @@ class LiveRoomController extends GetxController {
name: extra['reply_uname'],
);
}
messages.add(
addDm(
DanmakuMsg(
name: name,
uid: uid,
@@ -471,31 +496,17 @@ class LiveRoomController extends GetxController {
extra: liveExtra,
reply: reply,
),
DanmakuContentItem(
msg,
color: DanmakuOptions.blockColorful
? Colors.white
: DmUtils.decimalToColor(extra['color']),
type: DmUtils.getPosition(extra['mode']),
// extra['send_from_me'] is invalid
selfSend: isLogin && uid == mid,
extra: liveExtra,
),
);
if (plPlayerController.showDanmaku) {
danmakuController?.addDanmaku(
DanmakuContentItem(
msg,
color: DanmakuOptions.blockColorful
? Colors.white
: DmUtils.decimalToColor(extra['color']),
type: DmUtils.getPosition(extra['mode']),
// extra['send_from_me'] is invalid
selfSend: isLogin && uid == mid,
extra: liveExtra,
),
);
if (!disableAutoScroll.value) {
EasyThrottle.throttle(
'liveDm',
const Duration(milliseconds: 500),
() => WidgetsBinding.instance.addPostFrameCallback(
scrollToBottom,
),
);
}
}
break;
case 'SUPER_CHAT_MESSAGE' when showSuperChat:
final item = SuperChatItem.fromJson(obj['data']);
@@ -505,6 +516,7 @@ class LiveRoomController extends GetxController {
endTime: DateTime.now().millisecondsSinceEpoch ~/ 1000 + 10,
);
}
addDm(item);
break;
case 'WATCHED_CHANGE':
watchedShow.value = obj['data']['text_large'];
@@ -587,4 +599,26 @@ class LiveRoomController extends GetxController {
),
);
}
void reportSC(SuperChatItem item) {
if (!Accounts.main.isLogin) {
SmartDialog.showToast('账号未登录');
return;
}
autoWrapReportDialog(
Get.context!,
ReportOptions.liveDanmakuReport,
(reasonType, reasonDesc, banUid) {
return LiveHttp.superChatReport(
id: item.id,
roomId: roomId,
uid: item.uid,
msg: item.message,
reason: ReportOptions.liveDanmakuReport['']![reasonType]!,
ts: item.ts,
token: item.token,
);
},
);
}
}

View File

@@ -128,21 +128,17 @@ class _ReplyPageState extends CommonRichTextPubPageState<LiveSendDmPanel> {
),
Container(
height: 52,
padding: const EdgeInsets.only(left: 12, right: 12),
padding: const .symmetric(horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: .spaceBetween,
children: [
emojiBtn,
const Spacer(),
Obx(
() => FilledButton.tonal(
onPressed: enablePublish.value ? onPublish : null,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 10,
),
visualDensity: VisualDensity.compact,
visualDensity: .compact,
padding: const .symmetric(horizontal: 20, vertical: 10),
),
child: const Text('发送'),
),
@@ -195,7 +191,5 @@ class _ReplyPageState extends CommonRichTextPubPageState<LiveSendDmPanel> {
}
@override
Future<void> onMention([bool fromClick = false]) {
return Future.syncValue(null);
}
Future<void>? onMention([bool fromClick = false]) => null;
}

View File

@@ -3,7 +3,7 @@ import 'dart:async';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/models/common/image_type.dart';
import 'package:PiliPlus/models_new/live/live_superchat/item.dart';
import 'package:PiliPlus/pages/video/introduction/ugc/widgets/selectable_text.dart';
import 'package:PiliPlus/utils/platform_utils.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
@@ -12,13 +12,15 @@ class SuperChatCard extends StatefulWidget {
const SuperChatCard({
super.key,
required this.item,
required this.onRemove,
this.onRemove,
this.persistentSC = false,
required this.onReport,
});
final SuperChatItem item;
final VoidCallback onRemove;
final VoidCallback? onRemove;
final bool persistentSC;
final VoidCallback onReport;
@override
State<SuperChatCard> createState() => _SuperChatCardState();
@@ -40,7 +42,7 @@ class _SuperChatCardState extends State<SuperChatCard> {
final offset = widget.item.endTime - now;
if (offset > 0) {
_remains = offset.obs;
_timer = Timer.periodic(const Duration(seconds: 1), _callback);
_startTimer();
} else {
_remove();
}
@@ -56,7 +58,7 @@ class _SuperChatCardState extends State<SuperChatCard> {
void _onRemove() {
widget
..item.expired = true
..onRemove();
..onRemove?.call();
}
void _callback(_) {
@@ -69,6 +71,10 @@ class _SuperChatCardState extends State<SuperChatCard> {
}
}
void _startTimer() {
_timer = Timer.periodic(const Duration(seconds: 1), _callback);
}
void _cancelTimer() {
_timer?.cancel();
_timer = null;
@@ -80,61 +86,101 @@ class _SuperChatCardState extends State<SuperChatCard> {
super.dispose();
}
void _showMenu(Offset offset, SuperChatItem item) {
final flag = _timer != null;
if (flag) {
_cancelTimer();
}
showMenu(
context: context,
position: RelativeRect.fromLTRB(offset.dx, offset.dy, offset.dx, 0),
items: [
PopupMenuItem(
height: 38,
onTap: () => Get.toNamed('/member?mid=${item.uid}'),
child: Text(
'访问: ${item.userInfo.uname}',
style: const TextStyle(fontSize: 13),
),
),
PopupMenuItem(
height: 38,
onTap: widget.onReport,
child: const Text(
'举报',
style: TextStyle(fontSize: 13),
),
),
],
).whenComplete(() {
if (flag && mounted) {
_startTimer();
}
});
}
@override
Widget build(BuildContext context) {
final item = widget.item;
final bottomColor = Utils.parseColor(item.backgroundBottomColor);
final border = BorderSide(color: bottomColor);
void showMenu(TapUpDetails e) => _showMenu(e.globalPosition, item);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.vertical(top: Radius.circular(8)),
color: Utils.parseColor(item.backgroundColor),
border: Border(top: border, left: border, right: border),
),
padding: const EdgeInsets.all(8),
child: Row(
spacing: 12,
children: [
GestureDetector(
onTap: () => Get.toNamed('/member?mid=${item.uid}'),
child: NetworkImgLayer(
GestureDetector(
onTapUp: showMenu,
onSecondaryTapUp: PlatformUtils.isDesktop ? showMenu : null,
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.vertical(
top: Radius.circular(8),
),
color: Utils.parseColor(item.backgroundColor),
border: Border(top: border, left: border, right: border),
),
padding: const EdgeInsets.all(8),
child: Row(
spacing: 12,
children: [
NetworkImgLayer(
src: item.userInfo.face,
width: 45,
height: 45,
type: ImageType.avatar,
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
item.userInfo.uname,
style: TextStyle(
color: Utils.parseColor(item.userInfo.nameColor),
Expanded(
child: Column(
mainAxisSize: .min,
crossAxisAlignment: .start,
children: [
Text(
item.userInfo.uname,
style: TextStyle(
color: Utils.parseColor(item.userInfo.nameColor),
),
),
),
Text(
"${item.price}",
style: TextStyle(
color: Utils.parseColor(item.backgroundPriceColor),
Text(
"${item.price}",
style: TextStyle(
color: Utils.parseColor(item.backgroundPriceColor),
),
),
),
],
),
),
if (_remains != null)
Obx(
() => Text(
_remains.toString(),
style: const TextStyle(fontSize: 14, color: Colors.grey),
],
),
),
],
if (_remains != null)
Obx(
() => Text(
_remains.toString(),
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
),
],
),
),
),
Container(
@@ -145,9 +191,11 @@ class _SuperChatCardState extends State<SuperChatCard> {
color: bottomColor,
),
padding: const EdgeInsets.all(8),
child: selectableText(
item.message,
style: TextStyle(color: Utils.parseColor(item.messageFontColor)),
child: SelectionArea(
child: Text(
item.message,
style: TextStyle(color: Utils.parseColor(item.messageFontColor)),
),
),
),
],

View File

@@ -3,7 +3,7 @@ import 'package:PiliPlus/pages/live_room/controller.dart';
import 'package:PiliPlus/pages/live_room/superchat/superchat_card.dart';
import 'package:PiliPlus/pages/search/controller.dart';
import 'package:flutter/material.dart';
import 'package:get/get_state_manager/get_state_manager.dart';
import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart';
class SuperChatPanel extends StatefulWidget {
const SuperChatPanel({
@@ -48,9 +48,10 @@ class _SuperChatPanelState extends DebounceStreamState<SuperChatPanel, bool>
item: item,
onRemove: () => ctr?.add(true),
persistentSC: persistentSC,
onReport: () => widget.controller.reportSC(item),
);
},
separatorBuilder: (_, _) => const SizedBox(height: 12),
separatorBuilder: (_, _) => const SizedBox(height: 8),
),
);
}

View File

@@ -286,17 +286,10 @@ class _LiveRoomPageState extends State<LiveRoomPage>
right: 0,
child: TextButton(
onPressed: () {
_liveRoomController.fsSC.value = SuperChatItem.fromJson({
"id": Utils.random.nextInt(2147483647),
"price": 66,
"end_time":
DateTime.now().millisecondsSinceEpoch ~/ 1000 + 5,
"message": Utils.generateRandomString(55),
"user_info": {
"face": "",
"uname": Utils.generateRandomString(8),
},
});
final item = SuperChatItem.random;
_liveRoomController
..fsSC.value = item
..addDm(item);
},
child: const Text('add superchat'),
),
@@ -332,6 +325,7 @@ class _LiveRoomPageState extends State<LiveRoomPage>
child: SuperChatCard(
item: item,
onRemove: () => _liveRoomController.fsSC.value = null,
onReport: () => _liveRoomController.reportSC(item),
),
),
Positioned(
@@ -680,7 +674,7 @@ class _LiveRoomPageState extends State<LiveRoomPage>
clampDouble(maxHeight / maxWidth * 1.08, 0.56, 0.7) * maxWidth;
final rightWidth = min(400.0, maxWidth - videoWidth - padding.horizontal);
videoWidth = maxWidth - rightWidth - padding.horizontal;
final videoHeight = maxHeight - padding.top;
final videoHeight = maxHeight - padding.top - kToolbarHeight;
final width = isFullScreen ? maxWidth : videoWidth;
final height = isFullScreen ? maxHeight - padding.top : videoHeight;
return Padding(
@@ -1023,22 +1017,20 @@ class _LiveDanmakuState extends State<LiveDanmaku> {
@override
Widget build(BuildContext context) {
return Obx(
() {
return AnimatedOpacity(
opacity: plPlayerController.enableShowDanmaku.value
? plPlayerController.danmakuOpacity.value
: 0,
duration: const Duration(milliseconds: 100),
child: DanmakuScreen<DanmakuExtra>(
createdController: (e) {
widget.liveRoomController.danmakuController =
plPlayerController.danmakuController = e;
},
option: DanmakuOptions.get(notFullscreen: widget.notFullscreen),
size: widget.size,
),
);
},
() => AnimatedOpacity(
opacity: plPlayerController.enableShowDanmaku.value
? plPlayerController.danmakuOpacity.value
: 0,
duration: const Duration(milliseconds: 100),
child: DanmakuScreen<DanmakuExtra>(
createdController: (e) {
widget.liveRoomController.danmakuController =
plPlayerController.danmakuController = e;
},
option: DanmakuOptions.get(notFullscreen: widget.notFullscreen),
size: widget.size,
),
),
);
}
}

View File

@@ -1,13 +1,14 @@
import 'package:PiliPlus/common/widgets/flutter/popup_menu.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/http/live.dart';
import 'package:PiliPlus/models/common/image_type.dart';
import 'package:PiliPlus/models_new/live/live_danmaku/danmaku_msg.dart';
import 'package:PiliPlus/models_new/live/live_superchat/item.dart';
import 'package:PiliPlus/pages/live_room/controller.dart';
import 'package:PiliPlus/pages/live_room/superchat/superchat_card.dart';
import 'package:PiliPlus/pages/video/widgets/header_control.dart';
import 'package:PiliPlus/utils/accounts.dart';
import 'package:PiliPlus/utils/extension/theme_ext.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
@@ -50,51 +51,74 @@ class LiveRoomChatPanel extends StatelessWidget {
key: const PageStorageKey('live-chat'),
padding: const EdgeInsets.symmetric(horizontal: 12),
controller: liveRoomController.scrollController,
separatorBuilder: (context, index) => const SizedBox(height: 8),
separatorBuilder: (_, _) => const SizedBox(height: 8),
itemCount: liveRoomController.messages.length,
physics: const ClampingScrollPhysics(),
itemBuilder: (context, index) {
itemBuilder: (_, index) {
final item = liveRoomController.messages[index];
return Align(
alignment: Alignment.centerLeft,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: bg,
borderRadius: const BorderRadius.all(Radius.circular(14)),
),
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: '${item.name}: ',
style: TextStyle(
color: nameColor,
fontSize: 14,
),
recognizer: item.uid == 0
? null
: (TapGestureRecognizer()
..onTap = () =>
_showMsgDialog(context, item)),
if (item is DanmakuMsg) {
return Align(
alignment: Alignment.centerLeft,
child: Builder(
builder: (itemContext) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
if (item.reply case final reply?)
TextSpan(
text: '@${reply.name} ',
style: TextStyle(color: primary, fontSize: 14),
recognizer: TapGestureRecognizer()
..onTap = () =>
Get.toNamed('/member?mid=${reply.mid}'),
decoration: BoxDecoration(
color: bg,
borderRadius: const BorderRadius.all(
Radius.circular(14),
),
_buildMsg(devicePixelRatio, item),
],
),
),
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: '${item.name}: ',
style: TextStyle(
color: nameColor,
fontSize: 14,
),
recognizer: item.uid == 0
? null
: (TapGestureRecognizer()
..onTapDown = (e) => _showMsgMenu(
context,
itemContext,
e,
item,
)),
),
if (item.reply case final reply?)
TextSpan(
text: '@${reply.name} ',
style: TextStyle(
color: primary,
fontSize: 14,
),
recognizer: TapGestureRecognizer()
..onTap = () =>
Get.toNamed('/member?mid=${reply.mid}'),
),
_buildMsg(devicePixelRatio, item),
],
),
),
);
},
),
),
);
);
}
if (item is SuperChatItem) {
return SuperChatCard(
item: item,
persistentSC: true,
onReport: () => liveRoomController.reportSC(item),
);
}
throw item.runtimeType;
},
),
),
@@ -104,20 +128,10 @@ class LiveRoomChatPanel extends StatelessWidget {
right: 0,
child: TextButton(
onPressed: () {
liveRoomController.superChatMsg.insert(
0,
SuperChatItem.fromJson({
"id": Utils.random.nextInt(2147483647),
"price": 66,
"end_time":
DateTime.now().millisecondsSinceEpoch ~/ 1000 + 5,
"message": "message message message message message",
"user_info": {
"face": "",
"uname": "UNAME",
},
}),
);
final item = SuperChatItem.random;
liveRoomController
..superChatMsg.insert(0, item)
..addDm(item);
},
child: const Text('add superchat'),
),
@@ -273,82 +287,89 @@ class LiveRoomChatPanel extends StatelessWidget {
}
}
void _showMsgDialog(BuildContext context, DanmakuMsg item) {
showDialog(
void _showMsgMenu(
BuildContext context,
BuildContext itemContext,
TapDownDetails details,
DanmakuMsg item,
) {
final dx = details.globalPosition.dx;
final renderBox = itemContext.findRenderObject() as RenderBox;
final dy = renderBox.localToGlobal(renderBox.size.bottomLeft(.zero)).dy;
final autoScroll =
liveRoomController.autoScroll &&
!liveRoomController.disableAutoScroll.value;
if (autoScroll) {
liveRoomController.autoScroll = false;
}
showMenu(
context: context,
builder: (context) => SimpleDialog(
clipBehavior: .hardEdge,
contentPadding: const .symmetric(vertical: 12),
constraints: const BoxConstraints(minWidth: 280, maxWidth: 320),
title: Column(
spacing: 4,
mainAxisSize: .min,
crossAxisAlignment: .start,
children: [
Text(
item.name,
style: const TextStyle(fontSize: 15),
),
Text(
item.text,
style: TextStyle(
fontSize: 13,
color: ColorScheme.of(context).outline,
),
),
],
position: RelativeRect.fromLTRB(dx, dy, dx, 0),
items: <PopupMenuEntry<Never>>[
CustomPopupMenuItem(
height: 38,
child: Text(
item.name,
style: const TextStyle(fontSize: 13),
),
),
children: [
ListTile(
dense: true,
onTap: () {
Get
..back()
..toNamed('/member?mid=${item.uid}');
},
title: const Text('去TA的个人空间', style: TextStyle(fontSize: 14)),
const CustomPopupMenuDivider(height: 1),
PopupMenuItem(
height: 38,
onTap: () => Get.toNamed('/member?mid=${item.uid}'),
child: const Text(
'去TA的个人空间',
style: TextStyle(fontSize: 13),
),
ListTile(
dense: true,
onTap: () {
Get.back();
onAtUser(item);
},
title: const Text('@TA', style: TextStyle(fontSize: 14)),
),
PopupMenuItem(
height: 38,
onTap: () => onAtUser(item),
child: const Text(
'@TA',
style: TextStyle(fontSize: 13),
),
ListTile(
dense: true,
title: const Text('屏蔽发送者', style: TextStyle(fontSize: 14)),
onTap: () async {
Get.back();
if (!Accounts.main.isLogin) return;
final res = await LiveHttp.liveShieldUser(
uid: item.uid,
roomid: roomId,
type: 1,
);
if (res.isSuccess) {
SmartDialog.showToast('屏蔽成功');
} else {
res.toast();
}
},
),
PopupMenuItem(
height: 38,
onTap: () async {
if (!Accounts.main.isLogin) return;
final res = await LiveHttp.liveShieldUser(
uid: item.uid,
roomid: roomId,
type: 1,
);
if (res.isSuccess) {
SmartDialog.showToast('屏蔽成功');
} else {
res.toast();
}
},
child: const Text(
'屏蔽发送者',
style: TextStyle(fontSize: 13),
),
ListTile(
dense: true,
title: const Text('举报选中弹幕', style: TextStyle(fontSize: 14)),
onTap: () {
Get.back();
HeaderControl.reportLiveDanmaku(
context,
roomId: roomId,
msg: item.text,
extra: item.extra,
);
},
),
PopupMenuItem(
height: 38,
onTap: () => HeaderControl.reportLiveDanmaku(
context,
roomId: roomId,
msg: item.text,
extra: item.extra,
),
],
),
);
child: const Text(
'举报选中弹幕',
style: TextStyle(fontSize: 13),
),
),
],
).whenComplete(() {
if (autoScroll && context.mounted) {
liveRoomController
..autoScroll = true
..scrollToBottom();
}
});
}
}