import 'package:extended_text_field/extended_text_field.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:provider/provider.dart'; import 'package:yumi/app/config/business_logic_strategy.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/app/constants/sc_room_msg_type.dart'; import 'package:yumi/app/constants/sc_screen.dart'; import 'package:yumi/app_localizations.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/services/general/sc_app_general_manager.dart'; import 'package:yumi/shared/business_logic/models/res/sc_room_emoji_res.dart'; import 'package:yumi/shared/business_logic/usecases/sc_case.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/shared/tools/sc_keybord_util.dart'; import 'package:yumi/ui_kit/widgets/room/room_emoji_asset_image.dart'; import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; ///聊天输入框 class RoomMsgInput extends StatefulWidget { final String? atTextContent; final bool initialShowEmoji; const RoomMsgInput({ super.key, this.atTextContent, this.initialShowEmoji = false, }); @override State createState() => _RoomMsgInputState(); } class _RoomMsgInputState extends State { bool showSend = false; bool showEmoji = false; int _selectedEmojiCategoryIndex = 0; String? _lastEmojiPanelLogSignature; final FocusNode msgNode = FocusNode(); final TextEditingController controller = TextEditingController(); BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy; Color get _panelBackgroundColor => Colors.white24; @override void initState() { super.initState(); showEmoji = widget.initialShowEmoji; if (widget.atTextContent != null) { controller.value = TextEditingValue(text: widget.atTextContent ?? ""); showSend = controller.text.trim().isNotEmpty; } } @override void dispose() { controller.dispose(); msgNode.dispose(); super.dispose(); } @override void didChangeDependencies() { super.didChangeDependencies(); if (showEmoji) { _loadRoomEmojisIfNeeded(); } } @override Widget build(BuildContext context) { final keyboardHeight = MediaQuery.of(context).viewInsets.bottom; return Scaffold( backgroundColor: Colors.transparent, resizeToAvoidBottomInset: false, body: Column( children: [ Expanded( child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { Navigator.pop(context); }, ), ), AnimatedPadding( duration: const Duration(milliseconds: 220), curve: Curves.easeOut, padding: EdgeInsets.only(bottom: keyboardHeight), child: _buildBottomPanel(context), ), ], ), ); } Widget _buildBottomPanel(BuildContext context) { return Container( color: Colors.black, child: SafeArea( top: false, child: Padding( padding: EdgeInsets.fromLTRB(25.w, 12.w, 25.w, 12.w), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ GestureDetector( behavior: HitTestBehavior.opaque, onTap: _toggleEmojiPanel, child: SizedBox( width: 24.w, height: 24.w, child: Image.asset( showEmoji ? _strategy.getSCMessageChatPageChatKeyboardIcon() : "sc_images/room/sc_icon_room_bottom_emoji.png", color: showEmoji ? Colors.white : null, fit: BoxFit.contain, ), ), ), SizedBox(width: 15.w), Expanded(child: _buildInputBar(context)), ], ), if (showEmoji) SizedBox(height: 12.w), if (showEmoji) _buildEmojiPanel(), ], ), ), ), ); } Widget _buildInputBar(BuildContext context) { return Container( decoration: BoxDecoration( color: _panelBackgroundColor, borderRadius: BorderRadius.circular(height(5)), ), child: Row( children: [ Expanded( child: ExtendedTextField( controller: controller, textDirection: widget.atTextContent != null ? TextDirection.ltr : null, specialTextSpanBuilder: AtTextSpanBuilder(), focusNode: msgNode, autofocus: !widget.initialShowEmoji, textInputAction: TextInputAction.send, onTap: () { if (showEmoji) { setState(() { showEmoji = false; }); } }, onSubmitted: (_) { _sendMessage(); }, onChanged: (s) { setState(() { showSend = controller.text.trim().isNotEmpty; }); }, decoration: InputDecoration( hintText: SCAppLocalizations.of(context)!.pleaseChatFfriendly, fillColor: Colors.transparent, hintStyle: TextStyle(color: Colors.white60, fontSize: sp(14)), contentPadding: EdgeInsets.symmetric(horizontal: 15.w), counterText: '', isDense: true, filled: false, focusColor: Colors.transparent, hoverColor: Colors.transparent, border: InputBorder.none, enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, disabledBorder: InputBorder.none, errorBorder: InputBorder.none, focusedErrorBorder: InputBorder.none, ), style: TextStyle( fontSize: ScreenUtil().setSp(14), color: Colors.white, ), ), ), GestureDetector( behavior: HitTestBehavior.opaque, onTap: _sendMessage, child: Opacity( opacity: showSend ? 1 : 0.5, child: Container( padding: EdgeInsets.all(4.w), width: 30.w, height: 30.w, child: Image.asset( "sc_images/room/sc_icon_room_message_send.png", ), ), ), ), SizedBox(width: 3.w), ], ), ); } Widget _buildEmojiPanel() { final categories = _resolveRoomEmojiCategories(context); _logEmojiPanelState(categories); if (categories.isEmpty) { return Container( height: 220.w, decoration: BoxDecoration( color: _panelBackgroundColor, borderRadius: BorderRadius.circular(height(5)), ), ); } final selectedIndex = _clampedEmojiCategoryIndex(categories); final selectedCategory = categories[selectedIndex]; return Container( height: 220.w, decoration: BoxDecoration( color: _panelBackgroundColor, borderRadius: BorderRadius.circular(height(5)), ), child: Column( children: [ Expanded( child: GridView.builder( key: ValueKey(selectedCategory.id), padding: EdgeInsets.fromLTRB(18.w, 14.w, 18.w, 10.w), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, crossAxisSpacing: 18.w, mainAxisSpacing: 12.w, ), itemCount: selectedCategory.items.length, itemBuilder: (context, index) { final emojiItem = selectedCategory.items[index]; return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { _sendRoomEmoji(emojiItem.sendResource); }, child: Center( child: RoomEmojiAssetImage( key: ValueKey(emojiItem.displayResource), asset: emojiItem.displayResource, width: 42.w, height: 42.w, ), ), ); }, ), ), _buildEmojiCategoryBar(categories, selectedIndex), ], ), ); } Widget _buildEmojiCategoryBar( List<_RoomEmojiCategory> categories, int selectedIndex, ) { return Container( height: 50.w, padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 7.w), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.08), border: Border( top: BorderSide(color: Colors.white.withValues(alpha: 0.08)), ), ), child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: categories.length, separatorBuilder: (_, __) => SizedBox(width: 10.w), itemBuilder: (context, index) { final category = categories[index]; final selected = index == selectedIndex; return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { if (selected) { return; } setState(() { _selectedEmojiCategoryIndex = index; }); }, child: AnimatedContainer( duration: const Duration(milliseconds: 160), curve: Curves.easeOut, width: 64.w, height: 36.w, decoration: BoxDecoration( color: selected ? Colors.white.withValues(alpha: 0.22) : Colors.white.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(18.w), border: Border.all( color: selected ? Colors.white.withValues(alpha: 0.30) : Colors.transparent, width: 1.w, ), ), child: Center( child: RoomEmojiAssetImage( key: ValueKey("emoji_category_${category.id}"), asset: category.iconResource, width: 26.w, height: 26.w, ), ), ), ); }, ), ); } void _toggleEmojiPanel() { if (showEmoji) { setState(() { showEmoji = false; }); FocusScope.of(context).requestFocus(msgNode); return; } SCKeybordUtil.hide(context); _roomEmojiInputDebugLog('toggle open: request remote emoji load'); _loadRoomEmojisIfNeeded(forceWhenEmpty: true); setState(() { showEmoji = true; }); } void _loadRoomEmojisIfNeeded({bool forceWhenEmpty = false}) { final manager = Provider.of(context, listen: false); _roomEmojiInputDebugLog( 'load if needed loaded=${manager.hasRoomEmojisLoaded} ' 'loading=${manager.isFetchingRoomEmojis} ' 'groups=${manager.roomEmojiGroups.length} ' 'sourceAll=${manager.roomEmojisLoadedFromAll}', ); final forceRefresh = forceWhenEmpty && manager.hasRoomEmojisLoaded && manager.roomEmojiGroups.isEmpty; manager.emojiAll(forceRefresh: forceRefresh); } List<_RoomEmojiCategory> _resolveRoomEmojiCategories(BuildContext context) { final generalManager = Provider.of(context); return generalManager.roomEmojiGroups .map(_RoomEmojiCategory.fromRemote) .where((category) => category.items.isNotEmpty) .toList(); } void _logEmojiPanelState(List<_RoomEmojiCategory> categories) { final manager = Provider.of(context, listen: false); final signature = 'show=$showEmoji loaded=${manager.hasRoomEmojisLoaded} ' 'loading=${manager.isFetchingRoomEmojis} ' 'sourceAll=${manager.roomEmojisLoadedFromAll} ' 'rawGroups=${manager.roomEmojiGroups.length} ' 'renderGroups=${categories.length} ' 'renderEmojiTotal=${_countRenderedEmojiItems(categories)}'; if (_lastEmojiPanelLogSignature == signature) { return; } _lastEmojiPanelLogSignature = signature; _roomEmojiInputDebugLog( 'panel state $signature sample=${_renderedEmojiCategorySample(categories)}', ); } int _clampedEmojiCategoryIndex(List<_RoomEmojiCategory> categories) { if (_selectedEmojiCategoryIndex < categories.length) { return _selectedEmojiCategoryIndex; } return categories.isEmpty ? 0 : categories.length - 1; } void _sendRoomEmoji(String emojiAsset) { final rtcProvider = Provider.of(context, listen: false); final currenRoom = rtcProvider.currenRoom; final currentUser = AccountStorage().getCurrentUser()?.userProfile; if (currenRoom == null || currentUser == null) { return; } final seatIndex = rtcProvider.userOnMaiInIndex(currentUser.id ?? ""); final msg = Msg( groupId: currenRoom.roomProfile?.roomProfile?.roomAccount ?? "", role: rtcProvider.currenRoom?.entrants?.roles ?? "", msg: emojiAsset, type: SCRoomMsgType.emoticons, user: currentUser, number: seatIndex, ); if (seatIndex > -1) { rtcProvider.starPlayEmoji(msg); } Provider.of( context, listen: false, ).dispatchMessage(msg, addLocal: seatIndex < 0); } void _sendMessage() { if (controller.text.trim().isEmpty) { return; } final rtcProvider = Provider.of(context, listen: false); final currenRoom = rtcProvider.currenRoom; if (currenRoom == null) { return; } Provider.of(context, listen: false).dispatchMessage( Msg( groupId: currenRoom.roomProfile?.roomProfile?.roomAccount ?? "", role: rtcProvider.currenRoom?.entrants?.roles ?? "", msg: controller.text, type: SCRoomMsgType.text, user: AccountStorage().getCurrentUser()?.userProfile, ), ); Navigator.pop(context); } } class _RoomEmojiCategory { const _RoomEmojiCategory({ required this.id, required this.iconResource, required this.items, }); factory _RoomEmojiCategory.fromRemote(SCRoomEmojiRes group) { final items = (group.emojis ?? const []) .map(_RoomEmojiItem.fromRemote) .where((item) => item.sendResource.isNotEmpty) .toList(); final firstPreview = items.isNotEmpty ? items.first.displayResource.trim() : ""; final cover = group.cover?.trim() ?? ""; final id = (group.id ?? group.groupCode ?? group.groupName ?? firstPreview).trim(); return _RoomEmojiCategory( id: id.isEmpty ? firstPreview : id, iconResource: cover.isNotEmpty ? cover : firstPreview, items: items, ); } final String id; final String iconResource; final List<_RoomEmojiItem> items; } class _RoomEmojiItem { const _RoomEmojiItem({ required this.displayResource, required this.sendResource, }); factory _RoomEmojiItem.fromRemote(Emojis emoji) { final display = emoji.previewUrl.trim(); final send = emoji.sendUrl.trim(); return _RoomEmojiItem( displayResource: display.isNotEmpty ? display : send, sendResource: send.isNotEmpty ? send : display, ); } final String displayResource; final String sendResource; } void _roomEmojiInputDebugLog(String message) { debugPrint('[RoomEmoji][input] $message'); } int _countRenderedEmojiItems(List<_RoomEmojiCategory> categories) { return categories.fold( 0, (total, category) => total + category.items.length, ); } String _renderedEmojiCategorySample(List<_RoomEmojiCategory> categories) { if (categories.isEmpty) { return '[]'; } return categories .take(3) .map((category) { final first = category.items.isNotEmpty ? category.items.first : null; return '{id=${category.id}, icon=${category.iconResource}, ' 'count=${category.items.length}, firstDisplay=${first?.displayResource}, ' 'firstSend=${first?.sendResource}}'; }) .join(', '); } class PopRoute extends PopupRoute { final Duration _duration = Duration(milliseconds: 350); Widget child; PopRoute({required this.child}); @override Color? get barrierColor => null; @override bool get barrierDismissible => true; @override String? get barrierLabel => null; @override Widget buildPage( BuildContext context, Animation animation, Animation secondaryAnimation, ) { return child; } @override Duration get transitionDuration => _duration; }