diff --git a/lib/common/widgets/dialog/report.dart b/lib/common/widgets/dialog/report.dart index cebbefc89..cdc66e49d 100644 --- a/lib/common/widgets/dialog/report.dart +++ b/lib/common/widgets/dialog/report.dart @@ -10,8 +10,9 @@ Future autoWrapReportDialog( BuildContext context, Map> options, Future Function(int reasonType, String? reasonDesc, bool banUid) - onSuccess, -) { + onSuccess, { + bool ban = true, +}) { int? reasonType; String? reasonDesc; bool banUid = false; @@ -69,6 +70,8 @@ Future autoWrapReportDialog( labelText: '为帮助审核人员更快处理,请补充问题类型和出现位置等详细信息', border: OutlineInputBorder(), contentPadding: .all(10), + labelStyle: TextStyle(fontSize: 14), + floatingLabelStyle: TextStyle(fontSize: 14), ), onChanged: (value) => reasonDesc = value, validator: (value) => @@ -81,7 +84,7 @@ Future autoWrapReportDialog( ), ), ), - if (options != ReportOptions.liveDanmakuReport) + if (ban) Padding( padding: const EdgeInsets.only(left: 14, top: 6), child: CheckBoxText( @@ -251,4 +254,16 @@ abstract final class ReportOptions { 7: '其他', // avoid show form }, }; + + static Map> get imMsgReport => const { + '': { + 1: '色情低俗', + 2: '政治敏感', + 3: '违法有害', + 4: '广告骚扰', + 5: '人身攻击', + 6: '诈骗', + 0: '其他问题', + }, + }; } diff --git a/lib/common/widgets/flutter/list_tile.dart b/lib/common/widgets/flutter/list_tile.dart index 4d763bd57..e23ee084d 100644 --- a/lib/common/widgets/flutter/list_tile.dart +++ b/lib/common/widgets/flutter/list_tile.dart @@ -337,6 +337,7 @@ class ListTile extends StatelessWidget { this.onTap, this.onLongPress, this.onSecondaryTap, + this.onSecondaryTapUp, this.onFocusChange, this.mouseCursor, this.selected = false, @@ -569,6 +570,8 @@ class ListTile extends StatelessWidget { final GestureTapCallback? onSecondaryTap; + final GestureTapUpCallback? onSecondaryTapUp; + /// {@macro flutter.material.inkwell.onFocusChange} final ValueChanged? onFocusChange; @@ -983,6 +986,7 @@ class ListTile extends StatelessWidget { onTap: enabled ? onTap : null, onLongPress: enabled ? onLongPress : null, onSecondaryTap: enabled ? onSecondaryTap : null, + onSecondaryTapUp: enabled ? onSecondaryTapUp : null, onFocusChange: onFocusChange, mouseCursor: effectiveMouseCursor, canRequestFocus: enabled, diff --git a/lib/http/api.dart b/lib/http/api.dart index 1a565eaa4..75c573352 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -984,4 +984,6 @@ abstract final class Api { static const String superChatReport = '${HttpString.liveBaseUrl}/av/v1/SuperChat/report'; + + static const String imMsgReport = '${HttpString.tUrl}/x/bplus/im/report/add'; } diff --git a/lib/http/msg.dart b/lib/http/msg.dart index cbda879e1..aa3a217c1 100644 --- a/lib/http/msg.dart +++ b/lib/http/msg.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:PiliPlus/http/api.dart'; import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/init.dart'; @@ -609,4 +611,33 @@ abstract final class MsgHttp { return Error(res.data['message']); } } + + static Future> imMsgReport({ + required Object accusedUid, + required int reasonType, + required String reasonDesc, + required Map comment, + required Map extra, + }) async { + final res = await Request().post( + Api.imMsgReport, + data: { + 'biz_code': 4, + 'accused_uid': accusedUid, + 'object_id': accusedUid, + 'reason_type': reasonType, + 'reason_desc': reasonDesc, + 'module': 604, + 'comment': jsonEncode(comment), + 'extra': jsonEncode(extra), + 'csrf': Accounts.main.csrf, + }, + options: Options(contentType: Headers.formUrlEncodedContentType), + ); + if (res.data['code'] == 0) { + return const Success(null); + } else { + return Error(res.data['message']); + } + } } diff --git a/lib/models/dynamics/result.dart b/lib/models/dynamics/result.dart index c8fbc6bdf..423dd3fb6 100644 --- a/lib/models/dynamics/result.dart +++ b/lib/models/dynamics/result.dart @@ -248,9 +248,9 @@ class ModuleDispute { String? jumpUrl; ModuleDispute.fromJson(Map json) { - title=json['title']; - desc=json['desc']; - jumpUrl=json['jump_url']; + title = json['title']; + desc = json['desc']; + jumpUrl = json['jump_url']; } } diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index 842ca54ac..373e23a3d 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -607,6 +607,7 @@ class LiveRoomController extends GetxController { } autoWrapReportDialog( Get.context!, + ban: false, ReportOptions.liveDanmakuReport, (reasonType, reasonDesc, banUid) { return LiveHttp.superChatReport( diff --git a/lib/pages/live_room/widgets/chat_panel.dart b/lib/pages/live_room/widgets/chat_panel.dart index debb77762..497007606 100644 --- a/lib/pages/live_room/widgets/chat_panel.dart +++ b/lib/pages/live_room/widgets/chat_panel.dart @@ -84,7 +84,7 @@ class LiveRoomChatPanel extends StatelessWidget { recognizer: item.uid == 0 ? null : (TapGestureRecognizer() - ..onTapDown = (e) => _showMsgMenu( + ..onTapUp = (e) => _showMsgMenu( context, itemContext, e, @@ -290,7 +290,7 @@ class LiveRoomChatPanel extends StatelessWidget { void _showMsgMenu( BuildContext context, BuildContext itemContext, - TapDownDetails details, + TapUpDetails details, DanmakuMsg item, ) { final dx = details.globalPosition.dx; diff --git a/lib/pages/video/controller.dart b/lib/pages/video/controller.dart index 558e36d49..0b35370ff 100644 --- a/lib/pages/video/controller.dart +++ b/lib/pages/video/controller.dart @@ -855,6 +855,7 @@ class VideoDetailController extends GetxController } void initSkip() { + if (isClosed) return; if (segmentList.isNotEmpty) { positionSubscription?.cancel(); positionSubscription = plPlayerController @@ -1174,6 +1175,8 @@ class VideoDetailController extends GetxController mediaType: isFileSource ? entry.mediaType : null, ); + if (isClosed) return; + if (!isFileSource) { if (plPlayerController.enableBlock) { initSkip(); @@ -1482,7 +1485,7 @@ class VideoDetailController extends GetxController final result = await VideoHttp.vttSubtitles( subtitles[index - 1].subtitleUrl!, ); - if (result != null) { + if (!isClosed && result != null) { vttSubtitles[index - 1] = result; await setSub(result); } diff --git a/lib/pages/video/widgets/header_control.dart b/lib/pages/video/widgets/header_control.dart index 3635a648a..cde688159 100644 --- a/lib/pages/video/widgets/header_control.dart +++ b/lib/pages/video/widgets/header_control.dart @@ -282,6 +282,7 @@ class HeaderControl extends StatefulWidget { if (Accounts.main.isLogin) { return autoWrapReportDialog( context, + ban: false, ReportOptions.liveDanmakuReport, (reasonType, reasonDesc, banUid) { // if (banUid) { diff --git a/lib/pages/whisper/widgets/item.dart b/lib/pages/whisper/widgets/item.dart index 4f2b4ce25..b5aa71028 100644 --- a/lib/pages/whisper/widgets/item.dart +++ b/lib/pages/whisper/widgets/item.dart @@ -46,55 +46,6 @@ class WhisperSessionItem extends StatelessWidget { : null; final ThemeData theme = Theme.of(context); - void onLongPress() => showDialog( - context: context, - builder: (context) { - return AlertDialog( - clipBehavior: Clip.hardEdge, - contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: DefaultTextStyle( - style: const TextStyle(fontSize: 14), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - dense: true, - onTap: () { - Get.back(); - onSetTop(item.isPinned, item.id); - }, - title: Text(item.isPinned ? '移除置顶' : '置顶'), - ), - if (item.id.privateId.hasTalkerUid()) - ListTile( - dense: true, - onTap: () { - Get.back(); - onSetMute(item.isMuted, item.id.privateId.talkerUid); - }, - title: Text('${item.isMuted ? '关闭' : '开启'}免打扰'), - ), - if (item.id.privateId.hasTalkerUid()) - ListTile( - dense: true, - onTap: () { - Get.back(); - showConfirmDialog( - context: context, - title: '确定删除该对话?', - onConfirm: () => - onRemove(item.id.privateId.talkerUid.toInt()), - ); - }, - title: const Text('删除'), - ), - ], - ), - ), - ); - }, - ); - return ListTile( safeArea: true, tileColor: item.isPinned @@ -102,8 +53,88 @@ class WhisperSessionItem extends StatelessWidget { alpha: theme.brightness.isDark ? 0.4 : 0.8, ) : null, - onLongPress: onLongPress, - onSecondaryTap: PlatformUtils.isMobile ? null : onLongPress, + onLongPress: () => showDialog( + context: context, + builder: (context) { + return AlertDialog( + clipBehavior: Clip.hardEdge, + contentPadding: const EdgeInsets.symmetric(vertical: 12), + content: DefaultTextStyle( + style: const TextStyle(fontSize: 14), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + dense: true, + onTap: () { + Get.back(); + onSetTop(item.isPinned, item.id); + }, + title: Text(item.isPinned ? '移除置顶' : '置顶'), + ), + if (item.id.privateId.hasTalkerUid()) + ListTile( + dense: true, + onTap: () { + Get.back(); + onSetMute(item.isMuted, item.id.privateId.talkerUid); + }, + title: Text('${item.isMuted ? '关闭' : '开启'}免打扰'), + ), + if (item.id.privateId.hasTalkerUid()) + ListTile( + dense: true, + onTap: () { + Get.back(); + showConfirmDialog( + context: context, + title: '确定删除该对话?', + onConfirm: () => + onRemove(item.id.privateId.talkerUid.toInt()), + ); + }, + title: const Text('删除'), + ), + ], + ), + ), + ); + }, + ), + onSecondaryTapUp: PlatformUtils.isDesktop + ? (details) { + final offset = details.globalPosition; + showMenu( + context: context, + position: .fromLTRB(offset.dx, offset.dy, offset.dx, 0), + items: [ + PopupMenuItem( + height: 42, + onTap: () => onSetTop(item.isPinned, item.id), + child: Text(item.isPinned ? '移除置顶' : '置顶'), + ), + if (item.id.privateId.hasTalkerUid()) + PopupMenuItem( + height: 42, + onTap: () => + onSetMute(item.isMuted, item.id.privateId.talkerUid), + child: Text('${item.isMuted ? '关闭' : '开启'}免打扰'), + ), + if (item.id.privateId.hasTalkerUid()) + PopupMenuItem( + height: 42, + onTap: () => showConfirmDialog( + context: context, + title: '确定删除该对话?', + onConfirm: () => + onRemove(item.id.privateId.talkerUid.toInt()), + ), + child: const Text('删除'), + ), + ], + ); + } + : null, onTap: () { if (item.hasUnread()) { item.clearUnread(); diff --git a/lib/pages/whisper_detail/controller.dart b/lib/pages/whisper_detail/controller.dart index 6fa509f56..e2a21d635 100644 --- a/lib/pages/whisper_detail/controller.dart +++ b/lib/pages/whisper_detail/controller.dart @@ -131,4 +131,14 @@ class WhisperDetailController extends CommonListController { beginSeqno: msgSeqno != null ? Int64.ZERO : null, endSeqno: msgSeqno, ); + + Future onReport(Msg item, int reasonType, String reasonDesc) { + return MsgHttp.imMsgReport( + accusedUid: item.senderUid, + reasonType: reasonType, + reasonDesc: reasonDesc, + comment: {'group_id': 0, 'msg_key': item.msgKey}, + extra: {"msg_keys": []}, + ); + } } diff --git a/lib/pages/whisper_detail/view.dart b/lib/pages/whisper_detail/view.dart index 20b071306..c96f3bffa 100644 --- a/lib/pages/whisper_detail/view.dart +++ b/lib/pages/whisper_detail/view.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io' show File; +import 'package:PiliPlus/common/widgets/dialog/report.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/text_field.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; @@ -168,14 +169,18 @@ class _WhisperDetailPageState _whisperDetailController.onLoadMore(); } final item = response[index]; + final isOwner = + item.senderUid.toInt() == + _whisperDetailController.account.mid; return ChatItem( item: item, eInfos: _whisperDetailController.eInfos, - onLongPress: - item.senderUid.toInt() == - _whisperDetailController.account.mid - ? () => onLongPress(index, item) + onLongPress: () => onLongPress(index, item, isOwner), + onSecondaryTapUp: PlatformUtils.isDesktop + ? (e) => + _showMenu(e.globalPosition, index, item, isOwner) : null, + isOwner: isOwner, ); }, separatorBuilder: (context, index) => @@ -189,34 +194,86 @@ class _WhisperDetailPageState }; } - void onLongPress(int index, Msg item) { + void _showMenu(Offset offset, int index, Msg item, bool isOwner) { + showMenu( + context: context, + position: .fromLTRB(offset.dx, offset.dy, offset.dx, 0), + items: [ + if (isOwner) + PopupMenuItem( + height: 42, + onTap: () => _whisperDetailController.sendMsg( + message: '${item.msgKey}', + onClearText: editController.clear, + msgType: 5, + index: index, + ), + child: const Text('撤回', style: TextStyle(fontSize: 14)), + ) + else + PopupMenuItem( + height: 42, + onTap: () => autoWrapReportDialog( + context, + ban: false, + ReportOptions.imMsgReport, + (reasonType, reasonDesc, banUid) => + _whisperDetailController.onReport( + item, + reasonType, + reasonType == 0 + ? reasonDesc! + : ReportOptions.imMsgReport['']![reasonType]!, + ), + ), + child: const Text('举报', style: TextStyle(fontSize: 14)), + ), + ], + ); + } + + void onLongPress(int index, Msg item, bool isOwner) { + Feedback.forLongPress(context); showDialog( context: context, builder: (context) { return AlertDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - onTap: () { - Get.back(); - _whisperDetailController.sendMsg( - message: '${item.msgKey}', - onClearText: editController.clear, - msgType: 5, - index: index, - ); - }, - dense: true, - title: const Text( - '撤回', - style: TextStyle(fontSize: 14), + content: isOwner + ? ListTile( + onTap: () { + Get.back(); + _whisperDetailController.sendMsg( + message: '${item.msgKey}', + onClearText: editController.clear, + msgType: 5, + index: index, + ); + }, + dense: true, + title: const Text('撤回', style: TextStyle(fontSize: 14)), + ) + : ListTile( + onTap: () { + Get.back(); + autoWrapReportDialog( + context, + ban: false, + ReportOptions.imMsgReport, + (reasonType, reasonDesc, banUid) => + _whisperDetailController.onReport( + item, + reasonType, + reasonType == 0 + ? reasonDesc! + : ReportOptions.imMsgReport['']![reasonType]!, + ), + ); + }, + dense: true, + title: const Text('举报', style: TextStyle(fontSize: 14)), ), - ), - ], - ), ); }, ); diff --git a/lib/pages/whisper_detail/widget/chat_item.dart b/lib/pages/whisper_detail/widget/chat_item.dart index 749281a7f..dc5012bdd 100644 --- a/lib/pages/whisper_detail/widget/chat_item.dart +++ b/lib/pages/whisper_detail/widget/chat_item.dart @@ -18,7 +18,6 @@ import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; -import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -33,13 +32,16 @@ class ChatItem extends StatelessWidget { const ChatItem({ super.key, required this.item, - this.eInfos, - this.onLongPress, - }) : isOwner = onLongPress != null; + required this.eInfos, + required this.onLongPress, + required this.onSecondaryTapUp, + required this.isOwner, + }); final Msg item; final List? eInfos; - final VoidCallback? onLongPress; + final VoidCallback onLongPress; + final GestureTapUpCallback? onSecondaryTapUp; final bool isOwner; // 消息来源 @@ -51,14 +53,11 @@ class ChatItem extends StatelessWidget { // }; @override Widget build(BuildContext context) { - bool isPic = item.msgType == MsgType.EN_MSG_TYPE_PIC.value; // 图片 - bool isRevoke = item.msgType == MsgType.EN_MSG_TYPE_DRAW_BACK.value; // 撤回消息 - bool isSystem = - item.msgType == MsgType.EN_MSG_TYPE_VIDEO_CARD.value || - item.msgType == MsgType.EN_MSG_TYPE_TIP_MESSAGE.value || - item.msgType == MsgType.EN_MSG_TYPE_NOTIFY_MSG.value || - item.msgType == MsgType.EN_MSG_TYPE_PICTURE_CARD.value || - item.msgType == 16; + final msgType = item.msgType; + final isRevoke = msgType == MsgType.EN_MSG_TYPE_DRAW_BACK.value; // 撤回消息 + if (isRevoke) { + return const SizedBox.shrink(); + } late final ThemeData theme = Theme.of(context); late final Color textColor = isOwner @@ -66,108 +65,98 @@ class ChatItem extends StatelessWidget { : theme.colorScheme.onSurfaceVariant; late final dynamic content = jsonDecode(item.content); - return isRevoke - ? const SizedBox.shrink() - : Column( - children: [ - Padding( - padding: const EdgeInsets.only(top: 6, bottom: 18), - child: Text( - DateFormatUtils.chatFormat(item.timestamp.toInt()), - textAlign: TextAlign.center, - style: TextStyle(color: theme.colorScheme.outline), - ), - ), - isSystem - ? messageContent( - context: context, - theme: theme, - content: content, - textColor: textColor, + Widget child = messageContent( + context: context, + theme: theme, + content: content, + textColor: textColor, + ); + + final isSystem = + msgType == MsgType.EN_MSG_TYPE_VIDEO_CARD.value || + msgType == MsgType.EN_MSG_TYPE_TIP_MESSAGE.value || + msgType == MsgType.EN_MSG_TYPE_NOTIFY_MSG.value || + msgType == MsgType.EN_MSG_TYPE_PICTURE_CARD.value || + msgType == 16; + + if (!isSystem) { + final isPic = msgType == MsgType.EN_MSG_TYPE_PIC.value; // 图片 + child = Row( + mainAxisAlignment: isOwner ? .end : .start, + children: [ + Container( + constraints: const BoxConstraints(maxWidth: 300.0), + decoration: BoxDecoration( + color: isOwner + ? theme.colorScheme.secondaryContainer + : theme.colorScheme.onInverseSurface, + borderRadius: isOwner + ? const .only( + topLeft: .circular(16), + topRight: .circular(16), + bottomLeft: .circular(16), + bottomRight: .circular(6), ) - : GestureDetector( - onLongPress: () { - Feedback.forLongPress(context); - onLongPress!(); - }, - onSecondaryTap: PlatformUtils.isMobile - ? null - : onLongPress, - child: Row( - mainAxisAlignment: isOwner - ? MainAxisAlignment.end - : MainAxisAlignment.start, - children: [ - Container( - constraints: const BoxConstraints(maxWidth: 300.0), - decoration: BoxDecoration( - color: isOwner - ? theme.colorScheme.secondaryContainer - : theme.colorScheme.onInverseSurface, - borderRadius: isOwner - ? const BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - bottomLeft: Radius.circular(16), - bottomRight: Radius.circular(6), - ) - : const BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - bottomLeft: Radius.circular(6), - bottomRight: Radius.circular(16), - ), - ), - padding: EdgeInsets.only( - top: 8, - bottom: 6, - left: isPic ? 8 : 12, - right: isPic ? 8 : 12, - ), - child: Column( - crossAxisAlignment: isOwner - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - messageContent( - context: context, - theme: theme, - content: content, - textColor: textColor, - ), - SizedBox(height: isPic ? 7 : 2), - if (item.msgStatus == 1) - Text( - ' 已撤回', - style: theme.textTheme.labelSmall!.copyWith( - color: theme.colorScheme.onErrorContainer, - ), - ), - if (item.msgSource >= 8 && - item.msgSource <= 11) ...[ - Divider( - height: 10, - thickness: 1, - color: theme.colorScheme.outline.withValues( - alpha: 0.2, - ), - ), - Text( - '此条消息为自动回复', - style: theme.textTheme.labelMedium! - .copyWith( - color: theme.colorScheme.outline, - ), - ), - ], - ], - ), - ), - ], - ), + : const .only( + topLeft: .circular(16), + topRight: .circular(16), + bottomLeft: .circular(6), + bottomRight: .circular(16), ), - ], - ); + ), + padding: isPic + ? const .only(top: 8, bottom: 6, left: 8, right: 8) + : const .only(top: 8, bottom: 6, left: 12, right: 12), + child: Column( + crossAxisAlignment: isOwner ? .end : .start, + children: [ + child, + isPic ? const SizedBox(height: 7) : const SizedBox(height: 2), + if (item.msgStatus == 1) + Text( + ' 已撤回', + style: theme.textTheme.labelSmall!.copyWith( + color: theme.colorScheme.onErrorContainer, + ), + ), + if (item.msgSource >= 8 && item.msgSource <= 11) ...[ + Divider( + height: 10, + thickness: 1, + color: theme.colorScheme.outline.withValues(alpha: 0.2), + ), + Text( + '此条消息为自动回复', + style: theme.textTheme.labelMedium!.copyWith( + color: theme.colorScheme.outline, + ), + ), + ], + ], + ), + ), + ], + ); + } + + return Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 6, bottom: 18), + child: Text( + DateFormatUtils.chatFormat(item.timestamp.toInt()), + textAlign: TextAlign.center, + style: TextStyle(color: theme.colorScheme.outline), + ), + ), + GestureDetector( + behavior: .opaque, + onLongPress: onLongPress, + onSecondaryTapUp: onSecondaryTapUp, + child: child, + ), + ], + ); } Widget messageContent({ diff --git a/pubspec.lock b/pubspec.lock index 5e3f13c99..30d675d7f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -793,7 +793,7 @@ packages: description: path: "." ref: "version_4.7.2" - resolved-ref: "4f5c47f38bde5df0abd6481702b2d8ec199a0e49" + resolved-ref: "9addd004c11e1c388bff5988ac9564e7ee5ff395" url: "https://github.com/bggRGjQaUbCoE/getx.git" source: git version: "4.7.2"