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/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'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; class LiveRoomChatPanel extends StatelessWidget { const LiveRoomChatPanel({ super.key, required this.roomId, required this.liveRoomController, required this.isPP, required this.onAtUser, }); final int roomId; final LiveRoomController liveRoomController; final bool isPP; final ValueChanged onAtUser; bool get disableAutoScroll => liveRoomController.disableAutoScroll.value; @override Widget build(BuildContext context) { late final bg = isPP ? Colors.black.withValues(alpha: 0.4) : const Color(0x15FFFFFF); late final nameColor = isPP ? Colors.white.withValues(alpha: 0.9) : Colors.white.withValues(alpha: 0.6); late final devicePixelRatio = MediaQuery.devicePixelRatioOf(context); late final colorScheme = ColorScheme.of(context); late final primary = colorScheme.isDark ? colorScheme.primary : colorScheme.inversePrimary; return Stack( children: [ Obx( () => ListView.separated( key: const PageStorageKey('live-chat'), padding: const EdgeInsets.symmetric(horizontal: 12), controller: liveRoomController.scrollController, separatorBuilder: (context, index) => const SizedBox(height: 8), itemCount: liveRoomController.messages.length, physics: const ClampingScrollPhysics(), itemBuilder: (context, 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.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 (kDebugMode && liveRoomController.showSuperChat) ...[ Positioned( top: 50, 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", }, }), ); }, child: const Text('add superchat'), ), ), Positioned( right: 0, top: 90, child: TextButton( onPressed: () { if (liveRoomController.superChatMsg.isNotEmpty) { liveRoomController.superChatMsg.removeLast(); } }, child: const Text('remove superchat'), ), ), ], if (liveRoomController.showSuperChat) Positioned( top: 12, right: 12, child: Obx(() { final isEmpty = liveRoomController.superChatMsg.isEmpty; return AnimatedOpacity( opacity: isEmpty ? 0 : 1, duration: const Duration(milliseconds: 120), child: GestureDetector( onTap: isEmpty ? null : () => liveRoomController.pageController?.animateToPage( 1, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut, ), child: Container( decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(8)), color: const Color(0x2FFFFFFF), border: Border.all(color: Colors.white24, width: 0.7), ), padding: const EdgeInsets.fromLTRB(10, 4, 4, 4), child: Text.rich( style: const TextStyle(color: Colors.white, height: 1), strutStyle: const StrutStyle(height: 1, leading: 0), TextSpan( children: [ TextSpan( text: 'SC(${liveRoomController.superChatMsg.length})', ), const WidgetSpan( alignment: PlaceholderAlignment.middle, child: Icon( size: 18, Icons.keyboard_arrow_right, color: Colors.white, ), ), ], ), ), ), ), ); }), ), Obx( () => liveRoomController.disableAutoScroll.value ? Positioned( right: 12, bottom: 0, child: ElevatedButton.icon( style: ElevatedButton.styleFrom( visualDensity: VisualDensity.comfortable, ), icon: const Icon( Icons.arrow_downward_rounded, size: 20, ), label: const Text('回到底部'), onPressed: () => liveRoomController ..disableAutoScroll.value = false ..jumpToBottom(), ), ) : const SizedBox.shrink(), ), ], ); } InlineSpan _buildMsg(double devicePixelRatio, DanmakuMsg obj) { final uemote = obj.uemote; if (uemote != null) { // "room_{{room_id}}_{{int}}" or "upower_[{{emote}}]" final isUpower = uemote.isUpower; return WidgetSpan( child: NetworkImgLayer( src: uemote.url, type: ImageType.emote, width: isUpower ? uemote.width : uemote.width / devicePixelRatio, height: uemote.height == null ? null : isUpower ? uemote.height! : uemote.height! / devicePixelRatio, ), ); } final emots = obj.emots; if (emots != null) { RegExp regExp = RegExp(emots.keys.map(RegExp.escape).join('|')); final List spanChildren = []; obj.text.splitMapJoin( regExp, onMatch: (match) { final key = match[0]!; final emote = emots[key]!; spanChildren.add( WidgetSpan( child: NetworkImgLayer( src: emote.url, type: ImageType.emote, width: emote.width, height: emote.height, ), ), ); return ''; }, onNonMatch: (String nonMatchStr) { spanChildren.add( TextSpan( text: nonMatchStr, style: const TextStyle( color: Colors.white, fontSize: 14, ), ), ); return ''; }, ); return TextSpan(children: spanChildren); } else { return TextSpan( text: obj.text, style: const TextStyle( color: Colors.white, fontSize: 14, ), ); } } void _showMsgDialog(BuildContext context, DanmakuMsg item) { showDialog( 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, ), ), ], ), children: [ ListTile( dense: true, onTap: () { Get ..back() ..toNamed('/member?mid=${item.uid}'); }, title: const Text('去TA的个人空间', style: TextStyle(fontSize: 14)), ), ListTile( dense: true, onTap: () { Get.back(); onAtUser(item); }, title: const Text('@TA', style: TextStyle(fontSize: 14)), ), 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(); } }, ), ListTile( dense: true, title: const Text('举报选中弹幕', style: TextStyle(fontSize: 14)), onTap: () { Get.back(); HeaderControl.reportLiveDanmaku( context, roomId: roomId, msg: item.text, extra: item.extra, ); }, ), ], ), ); } }