diff --git a/lib/common/widgets/dialog/report.dart b/lib/common/widgets/dialog/report.dart index 1c7f4f698..cebbefc89 100644 --- a/lib/common/widgets/dialog/report.dart +++ b/lib/common/widgets/dialog/report.dart @@ -81,13 +81,14 @@ Future autoWrapReportDialog( ), ), ), - Padding( - padding: const EdgeInsets.only(left: 14, top: 6), - child: CheckBoxText( - text: '拉黑该用户', - onChanged: (value) => banUid = value, + if (options != ReportOptions.liveDanmakuReport) + Padding( + padding: const EdgeInsets.only(left: 14, top: 6), + child: CheckBoxText( + text: '拉黑该用户', + onChanged: (value) => banUid = value, + ), ), - ), ], ), actions: [ diff --git a/lib/common/widgets/flutter/popup_menu.dart b/lib/common/widgets/flutter/popup_menu.dart new file mode 100644 index 000000000..1e05da9cc --- /dev/null +++ b/lib/common/widgets/flutter/popup_menu.dart @@ -0,0 +1,157 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +library; + +import 'package:flutter/material.dart'; + +class CustomPopupMenuItem extends PopupMenuEntry { + const CustomPopupMenuItem({ + super.key, + this.value, + this.height = kMinInteractiveDimension, + required this.child, + }); + + final T? value; + + @override + final double height; + + final Widget? child; + + @override + bool represents(T? value) => value == this.value; + + @override + CustomPopupMenuItemState> createState() => + CustomPopupMenuItemState>(); +} + +class CustomPopupMenuItemState> + extends State { + @protected + @override + Widget build(BuildContext context) { + final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); + const Set states = {}; + + final style = + popupMenuTheme.labelTextStyle?.resolve(states)! ?? + _PopupMenuDefaultsM3(context).labelTextStyle!.resolve(states)!; + + return ListTileTheme.merge( + contentPadding: .zero, + titleTextStyle: style, + child: AnimatedDefaultTextStyle( + style: style, + duration: kThemeChangeDuration, + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: widget.height), + child: Padding( + padding: _PopupMenuDefaultsM3.menuItemPadding, + child: Align( + alignment: AlignmentDirectional.centerStart, + child: widget.child, + ), + ), + ), + ), + ); + } +} + +class CustomPopupMenuDivider extends PopupMenuEntry { + const CustomPopupMenuDivider({ + super.key, + required this.height, + this.thickness, + this.indent, + this.endIndent, + this.radius, + }); + + @override + final double height; + + final double? thickness; + + final double? indent; + + final double? endIndent; + + final BorderRadiusGeometry? radius; + + @override + bool represents(void value) => false; + + @override + State createState() => _CustomPopupMenuDividerState(); +} + +class _CustomPopupMenuDividerState extends State { + @override + Widget build(BuildContext context) { + return Divider( + height: widget.height, + thickness: widget.thickness, + indent: widget.indent, + color: ColorScheme.of(context).outline.withValues(alpha: 0.2), + endIndent: widget.endIndent, + radius: widget.radius, + ); + } +} + +// BEGIN GENERATED TOKEN PROPERTIES - PopupMenu + +// Do not edit by hand. The code between the "BEGIN GENERATED" and +// "END GENERATED" comments are generated from data in the Material +// Design token database by the script: +// dev/tools/gen_defaults/bin/gen_defaults.dart. + +// dart format off +class _PopupMenuDefaultsM3 extends PopupMenuThemeData { + _PopupMenuDefaultsM3(this.context) + : super(elevation: 3.0); + + final BuildContext context; + late final ThemeData _theme = Theme.of(context); + late final ColorScheme _colors = _theme.colorScheme; + late final TextTheme _textTheme = _theme.textTheme; + + @override WidgetStateProperty? get labelTextStyle { + return WidgetStateProperty.resolveWith((Set states) { + // TODO(quncheng): Update this hard-coded value to use the latest tokens. + final TextStyle style = _textTheme.labelLarge!; + if (states.contains(WidgetState.disabled)) { + return style.apply(color: _colors.onSurface.withValues(alpha: 0.38)); + } + return style.apply(color: _colors.onSurface); + }); + } + + @override + Color? get color => _colors.surfaceContainer; + + @override + Color? get shadowColor => _colors.shadow; + + @override + Color? get surfaceTintColor => Colors.transparent; + + @override + ShapeBorder? get shape => const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0))); + + // TODO(bleroux): This is taken from https://m3.material.io/components/menus/specs + // Update this when the token is available. + @override + EdgeInsets? get menuPadding => const EdgeInsets.symmetric(vertical: 8.0); + + // TODO(tahatesser): This is taken from https://m3.material.io/components/menus/specs + // Update this when the token is available. + static EdgeInsets menuItemPadding = const EdgeInsets.symmetric(horizontal: 12.0); +}// dart format on + +// END GENERATED TOKEN PROPERTIES - PopupMenu diff --git a/lib/http/api.dart b/lib/http/api.dart index 4033d550e..1a565eaa4 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -981,4 +981,7 @@ abstract final class Api { static const String liveContributionRank = '${HttpString.liveBaseUrl}/xlive/general-interface/v1/rank/queryContributionRank'; + + static const String superChatReport = + '${HttpString.liveBaseUrl}/av/v1/SuperChat/report'; } diff --git a/lib/http/live.dart b/lib/http/live.dart index 1fcaa9601..22daae26d 100644 --- a/lib/http/live.dart +++ b/lib/http/live.dart @@ -695,4 +695,40 @@ abstract final class LiveHttp { return Error(res.data['message']); } } + + static Future> superChatReport({ + required int id, + required Object roomId, + required Object uid, + required String msg, + required String reason, + required int ts, + required String token, + }) async { + final csrf = Accounts.main.csrf; + final res = await Request().post( + Api.superChatReport, + data: { + 'id': id, + 'roomid': roomId, + 'uid': uid, + 'msg': msg, + 'reason': reason, + 'ts': ts, + 'sign': '', + 'reason_id': reason, + 'token': token, + 'id_str': id.toString(), + 'csrf_token': csrf, + 'csrf': csrf, + 'visit_id': '', + }, + 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_new/download/bili_download_entry_info.dart b/lib/models_new/download/bili_download_entry_info.dart index 77e63250a..e103de0b0 100644 --- a/lib/models_new/download/bili_download_entry_info.dart +++ b/lib/models_new/download/bili_download_entry_info.dart @@ -67,7 +67,7 @@ class BiliDownloadEntryInfo with MultiSelectData { ), itemBuilder: (_) => [ PopupMenuItem( - height: 35, + height: 38, child: const Text( '查看详情页', style: TextStyle(fontSize: 13), @@ -99,7 +99,7 @@ class BiliDownloadEntryInfo with MultiSelectData { ), if (ownerId case final mid?) PopupMenuItem( - height: 35, + height: 38, child: Text( '访问${ownerName != null ? ':$ownerName' : '用户主页'}', style: const TextStyle( diff --git a/lib/models_new/live/live_superchat/item.dart b/lib/models_new/live/live_superchat/item.dart index 5b811f483..8ce5895e2 100644 --- a/lib/models_new/live/live_superchat/item.dart +++ b/lib/models_new/live/live_superchat/item.dart @@ -3,40 +3,60 @@ import 'package:PiliPlus/utils/utils.dart'; class SuperChatItem { int id; - int? uid; - int? price; + int uid; + int price; String backgroundColor; String backgroundBottomColor; String backgroundPriceColor; String messageFontColor; int endTime; String message; + String token; + int ts; UserInfo userInfo; bool expired = false; SuperChatItem({ required this.id, required this.uid, - this.price, + required this.price, required this.backgroundColor, required this.backgroundBottomColor, required this.backgroundPriceColor, required this.messageFontColor, required this.endTime, required this.message, + required this.token, + required this.ts, required this.userInfo, }); + static SuperChatItem get random => SuperChatItem.fromJson({ + "id": Utils.random.nextInt(2147483647), + "uid": 0, + "price": 66, + "end_time": DateTime.now().millisecondsSinceEpoch ~/ 1000 + 5, + "message": Utils.generateRandomString(55), + "user_info": { + "face": "", + "uname": "UNAME", + }, + 'token': '', + 'ts': 0, + }); + factory SuperChatItem.fromJson(Map json) => SuperChatItem( - id: json['id'] ?? Utils.random.nextInt(2147483647), - uid: json['uid'], - price: json['price'] as int?, + id: Utils.safeToInt(json['id']) ?? Utils.random.nextInt(2147483647), + uid: Utils.safeToInt(json['uid'])!, + price: json['price'], backgroundColor: json['background_color'] ?? '#EDF5FF', backgroundBottomColor: json['background_bottom_color'] ?? '#2A60B2', backgroundPriceColor: json['background_price_color'] ?? '#7497CD', messageFontColor: json['message_font_color'] ?? '#FFFFFF', - endTime: json['end_time'], + endTime: Utils.safeToInt(json['end_time'])!, message: json['message'], + token: json['token'], + ts: Utils.safeToInt(json['ts'])!, userInfo: UserInfo.fromJson(json['user_info'] as Map), ); @@ -50,6 +70,8 @@ class SuperChatItem { String? messageFontColor, int? endTime, String? message, + String? token, + int? ts, UserInfo? userInfo, bool? expired, }) { @@ -64,6 +86,8 @@ class SuperChatItem { messageFontColor: messageFontColor ?? this.messageFontColor, endTime: endTime ?? this.endTime, message: message ?? this.message, + token: token ?? this.token, + ts: ts ?? this.ts, userInfo: userInfo ?? this.userInfo, ); } diff --git a/lib/pages/common/publish/common_rich_text_pub_page.dart b/lib/pages/common/publish/common_rich_text_pub_page.dart index 699519f84..adc4c4245 100644 --- a/lib/pages/common/publish/common_rich_text_pub_page.dart +++ b/lib/pages/common/publish/common_rich_text_pub_page.dart @@ -274,7 +274,7 @@ abstract class CommonRichTextPubPageState } late double _mentionOffset = 0; - Future onMention([bool fromClick = false]) async { + Future? onMention([bool fromClick = false]) async { controller.keepChatPanel(); final res = await DynMentionPanel.onDynMention( context, diff --git a/lib/pages/history/view.dart b/lib/pages/history/view.dart index 7f75f24f8..15fac0121 100644 --- a/lib/pages/history/view.dart +++ b/lib/pages/history/view.dart @@ -157,56 +157,39 @@ class _HistoryPageState extends State onPressed: () => Get.toNamed('/historySearch'), icon: const Icon(Icons.search_outlined), ), - PopupMenuButton( - onSelected: (String type) { - switch (type) { - case 'pause': - _historyController.baseCtr.onPauseHistory( - context, - ); - break; - case 'clear': - _historyController.baseCtr.onClearHistory( - context, - () { - _historyController.loadingState.value = const Success( - null, - ); - if (_historyController.tabController != null) { - for (final item in _historyController.tabs) { - try { - Get.find( - tag: item.type, - ).loadingState.value = const Success( - null, - ); - } catch (_) {} - } - } - }, - ); - break; - case 'viewed': - currCtr().onDelViewedHistory(); - break; - } - }, - itemBuilder: (BuildContext context) => >[ - PopupMenuItem( - value: 'pause', + PopupMenuButton( + itemBuilder: (_) => [ + PopupMenuItem( + onTap: () => _historyController.baseCtr.onPauseHistory(context), child: Text( !_historyController.baseCtr.pauseStatus.value ? '暂停观看记录' : '恢复观看记录', ), ), - const PopupMenuItem( - value: 'clear', - child: Text('清空观看记录'), + PopupMenuItem( + onTap: () => _historyController.baseCtr.onClearHistory( + context, + () { + _historyController.loadingState.value = const Success(null); + if (_historyController.tabController != null) { + for (final item in _historyController.tabs) { + try { + Get.find( + tag: item.type, + ).loadingState.value = const Success( + null, + ); + } catch (_) {} + } + } + }, + ), + child: const Text('清空观看记录'), ), - const PopupMenuItem( - value: 'viewed', - child: Text('删除已看记录'), + PopupMenuItem( + onTap: currCtr().onDelViewedHistory, + child: const Text('删除已看记录'), ), ], ), diff --git a/lib/pages/history/widgets/item.dart b/lib/pages/history/widgets/item.dart index b39c0daae..2de6b6ef2 100644 --- a/lib/pages/history/widgets/item.dart +++ b/lib/pages/history/widgets/item.dart @@ -182,7 +182,7 @@ class HistoryItem extends StatelessWidget { child: SizedBox( width: 29, height: 29, - child: PopupMenuButton( + child: PopupMenuButton( padding: EdgeInsets.zero, tooltip: '功能菜单', icon: Icon( @@ -191,61 +191,60 @@ class HistoryItem extends StatelessWidget { size: 18, ), position: PopupMenuPosition.under, - itemBuilder: (BuildContext context) => - >[ - if (item.authorMid != null && - item.authorName?.isNotEmpty == true) - PopupMenuItem( - onTap: () => - Get.toNamed('/member?mid=${item.authorMid}'), - height: 35, - child: Row( - children: [ - const Icon( - MdiIcons.accountCircleOutline, - size: 16, - ), - const SizedBox(width: 6), - Text( - '访问:${item.authorName}', - style: const TextStyle(fontSize: 13), - ), - ], + itemBuilder: (_) => [ + if (item.authorMid != null && + item.authorName?.isNotEmpty == true) + PopupMenuItem( + onTap: () => + Get.toNamed('/member?mid=${item.authorMid}'), + height: 38, + child: Row( + children: [ + const Icon( + MdiIcons.accountCircleOutline, + size: 16, ), - ), - if (business != 'pgc' && - item.badge != '番剧' && - item.tagName?.contains('动画') != true && - business != 'live' && - business?.contains('article') != true) - PopupMenuItem( - onTap: () async { - final res = await UserHttp.toViewLater( - bvid: item.history.bvid, - ); - SmartDialog.showToast(res['msg']); - }, - height: 35, - child: const Row( - children: [ - Icon(Icons.watch_later_outlined, size: 16), - SizedBox(width: 6), - Text('稍后再看', style: TextStyle(fontSize: 13)), - ], + const SizedBox(width: 6), + Text( + '访问:${item.authorName}', + style: const TextStyle(fontSize: 13), ), - ), - PopupMenuItem( - onTap: () => onDelete(item.kid!, business!), - height: 35, - child: const Row( - children: [ - Icon(Icons.close_outlined, size: 16), - SizedBox(width: 6), - Text('删除记录', style: TextStyle(fontSize: 13)), - ], - ), + ], ), - ], + ), + if (business != 'pgc' && + item.badge != '番剧' && + item.tagName?.contains('动画') != true && + business != 'live' && + business?.contains('article') != true) + PopupMenuItem( + onTap: () async { + final res = await UserHttp.toViewLater( + bvid: item.history.bvid, + ); + SmartDialog.showToast(res['msg']); + }, + height: 38, + child: const Row( + children: [ + Icon(Icons.watch_later_outlined, size: 16), + SizedBox(width: 6), + Text('稍后再看', style: TextStyle(fontSize: 13)), + ], + ), + ), + PopupMenuItem( + onTap: () => onDelete(item.kid!, business!), + height: 38, + child: const Row( + children: [ + Icon(Icons.close_outlined, size: 16), + SizedBox(width: 6), + Text('删除记录', style: TextStyle(fontSize: 13)), + ], + ), + ), + ], ), ), ), diff --git a/lib/pages/live_dm_block/view.dart b/lib/pages/live_dm_block/view.dart index b4ab133b7..2bd5307d8 100644 --- a/lib/pages/live_dm_block/view.dart +++ b/lib/pages/live_dm_block/view.dart @@ -165,7 +165,7 @@ class _LiveDmBlockPageState extends State { Widget _buildKeyword(List list) { if (list.isEmpty) { - return isPortrait ? errorWidget() : scrollErrorWidget(); + return scrollErrorWidget(); } return SingleChildScrollView( padding: EdgeInsets.only( diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index adb14683e..842ca54ac 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -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? savedDanmaku; - RxList messages = [].obs; + RxList messages = [].obs; late final Rx fsSC = Rx(null); late final RxList superChatMsg = [].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(DanmakuMsg.fromPrefetch), ); - WidgetsBinding.instance.addPostFrameCallback(scrollToBottom); + scrollToBottom(); } catch (_) {} } } @@ -424,6 +436,19 @@ class LiveRoomController extends GetxController { ..init(); } + void addDm(dynamic msg, [DanmakuContentItem? 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, + ); + }, + ); + } } diff --git a/lib/pages/live_room/send_danmaku/view.dart b/lib/pages/live_room/send_danmaku/view.dart index 0a85e86fc..fa9148562 100644 --- a/lib/pages/live_room/send_danmaku/view.dart +++ b/lib/pages/live_room/send_danmaku/view.dart @@ -128,21 +128,17 @@ class _ReplyPageState extends CommonRichTextPubPageState { ), 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 { } @override - Future onMention([bool fromClick = false]) { - return Future.syncValue(null); - } + Future? onMention([bool fromClick = false]) => null; } diff --git a/lib/pages/live_room/superchat/superchat_card.dart b/lib/pages/live_room/superchat/superchat_card.dart index a6b6d34ca..70a83433c 100644 --- a/lib/pages/live_room/superchat/superchat_card.dart +++ b/lib/pages/live_room/superchat/superchat_card.dart @@ -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 createState() => _SuperChatCardState(); @@ -40,7 +42,7 @@ class _SuperChatCardState extends State { 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 { void _onRemove() { widget ..item.expired = true - ..onRemove(); + ..onRemove?.call(); } void _callback(_) { @@ -69,6 +71,10 @@ class _SuperChatCardState extends State { } } + void _startTimer() { + _timer = Timer.periodic(const Duration(seconds: 1), _callback); + } + void _cancelTimer() { _timer?.cancel(); _timer = null; @@ -80,61 +86,101 @@ class _SuperChatCardState extends State { 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 { 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)), + ), ), ), ], diff --git a/lib/pages/live_room/superchat/superchat_panel.dart b/lib/pages/live_room/superchat/superchat_panel.dart index d54162f08..d91ad0883 100644 --- a/lib/pages/live_room/superchat/superchat_panel.dart +++ b/lib/pages/live_room/superchat/superchat_panel.dart @@ -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 item: item, onRemove: () => ctr?.add(true), persistentSC: persistentSC, + onReport: () => widget.controller.reportSC(item), ); }, - separatorBuilder: (_, _) => const SizedBox(height: 12), + separatorBuilder: (_, _) => const SizedBox(height: 8), ), ); } diff --git a/lib/pages/live_room/view.dart b/lib/pages/live_room/view.dart index 622879fc5..d0e31073e 100644 --- a/lib/pages/live_room/view.dart +++ b/lib/pages/live_room/view.dart @@ -286,17 +286,10 @@ class _LiveRoomPageState extends State 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 child: SuperChatCard( item: item, onRemove: () => _liveRoomController.fsSC.value = null, + onReport: () => _liveRoomController.reportSC(item), ), ), Positioned( @@ -680,7 +674,7 @@ class _LiveRoomPageState extends State 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 { @override Widget build(BuildContext context) { return Obx( - () { - return AnimatedOpacity( - opacity: plPlayerController.enableShowDanmaku.value - ? plPlayerController.danmakuOpacity.value - : 0, - duration: const Duration(milliseconds: 100), - child: DanmakuScreen( - 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( + createdController: (e) { + widget.liveRoomController.danmakuController = + plPlayerController.danmakuController = e; + }, + option: DanmakuOptions.get(notFullscreen: widget.notFullscreen), + size: widget.size, + ), + ), ); } } diff --git a/lib/pages/live_room/widgets/chat_panel.dart b/lib/pages/live_room/widgets/chat_panel.dart index 1a0aaa838..debb77762 100644 --- a/lib/pages/live_room/widgets/chat_panel.dart +++ b/lib/pages/live_room/widgets/chat_panel.dart @@ -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: >[ + 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(); + } + }); } } diff --git a/lib/pages/setting/pages/logs.dart b/lib/pages/setting/pages/logs.dart index 98a97ee2f..5e92ca894 100644 --- a/lib/pages/setting/pages/logs.dart +++ b/lib/pages/setting/pages/logs.dart @@ -89,7 +89,7 @@ class _LogsPageState extends State { } } - Future clearLogsHandle() async { + Future clearLogs() async { if (await LoggerUtils.clearLogs()) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -112,57 +112,45 @@ class _LogsPageState extends State { appBar: AppBar( title: const Text('日志'), actions: [ - PopupMenuButton( - onSelected: (String type) { - switch (type) { - case 'log': - enableLog = !enableLog; - GStorage.setting.put(SettingBoxKey.enableLog, enableLog); - SmartDialog.showToast('已${enableLog ? '开启' : '关闭'},重启生效'); - break; - case 'copy': - copyLogs(); - break; - case 'feedback': - PageUtils.launchURL('${Constants.sourceCodeUrl}/issues'); - break; - case 'clear': - latestLog = null; - clearLogsHandle(); - break; - default: - if (kDebugMode) { - Timer.periodic(const Duration(milliseconds: 3500), (timer) { + PopupMenuButton( + itemBuilder: (_) => [ + if (kDebugMode) + PopupMenuItem( + onTap: () => Timer.periodic( + const Duration(milliseconds: 3500), + (timer) { Utils.reportError('Manual'); if (timer.tick > 3) { timer.cancel(); if (mounted) getLog(); } - }); - } - } - }, - itemBuilder: (BuildContext context) => >[ - if (kDebugMode) - const PopupMenuItem( - value: 'assert', - child: Text('引发错误'), + }, + ), + child: const Text('引发错误'), ), - PopupMenuItem( - value: 'log', + PopupMenuItem( + onTap: () { + enableLog = !enableLog; + GStorage.setting.put(SettingBoxKey.enableLog, enableLog); + SmartDialog.showToast('已${enableLog ? '开启' : '关闭'},重启生效'); + }, child: Text('${enableLog ? '关闭' : '开启'}日志'), ), - const PopupMenuItem( - value: 'copy', - child: Text('复制日志'), + PopupMenuItem( + onTap: copyLogs, + child: const Text('复制日志'), ), - const PopupMenuItem( - value: 'feedback', - child: Text('错误反馈'), + PopupMenuItem( + onTap: () => + PageUtils.launchURL('${Constants.sourceCodeUrl}/issues'), + child: const Text('错误反馈'), ), - const PopupMenuItem( - value: 'clear', - child: Text('清空日志'), + PopupMenuItem( + onTap: () { + latestLog = null; + clearLogs(); + }, + child: const Text('清空日志'), ), ], ), diff --git a/lib/pages/video/introduction/ugc/widgets/selectable_text.dart b/lib/pages/video/introduction/ugc/widgets/selectable_text.dart index 231d0e77e..235abda4f 100644 --- a/lib/pages/video/introduction/ugc/widgets/selectable_text.dart +++ b/lib/pages/video/introduction/ugc/widgets/selectable_text.dart @@ -16,6 +16,7 @@ Widget selectableText( return SelectableText( style: style, text, + scrollPhysics: const NeverScrollableScrollPhysics(), ); } @@ -34,5 +35,6 @@ Widget selectableRichText( return SelectableText.rich( style: style, textSpan, + scrollPhysics: const NeverScrollableScrollPhysics(), ); } diff --git a/lib/pages/whisper_detail/view.dart b/lib/pages/whisper_detail/view.dart index 010a3d06e..20b071306 100644 --- a/lib/pages/whisper_detail/view.dart +++ b/lib/pages/whisper_detail/view.dart @@ -359,9 +359,7 @@ class _WhisperDetailPageState } @override - Future onMention([bool fromClick = false]) { - return Future.syncValue(null); - } + Future? onMention([bool fromClick = false]) => null; @override void onSave() {}