From e927bf4520cf6d72080205009c3735002c670388 Mon Sep 17 00:00:00 2001 From: NIGGER SLAYER Date: Tue, 14 Apr 2026 14:12:22 +0800 Subject: [PATCH] =?UTF-8?q?emoji=E6=B7=BB=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/ui_kit/widgets/room/room_msg_input.dart | 528 ++++++++++++-------- 需求进度.md | 7 + 2 files changed, 323 insertions(+), 212 deletions(-) diff --git a/lib/ui_kit/widgets/room/room_msg_input.dart b/lib/ui_kit/widgets/room/room_msg_input.dart index f9f205f..b5bf07f 100644 --- a/lib/ui_kit/widgets/room/room_msg_input.dart +++ b/lib/ui_kit/widgets/room/room_msg_input.dart @@ -1,212 +1,316 @@ -import 'dart:ui' as ui; - -import 'package:extended_text_field/extended_text_field.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:yumi/app_localizations.dart'; -import 'package:yumi/app/constants/sc_room_msg_type.dart'; -import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; -import 'package:yumi/shared/business_logic/usecases/sc_case.dart'; -import 'package:provider/provider.dart'; -import 'package:yumi/app/constants/sc_screen.dart'; -import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; -import 'package:yumi/services/audio/rtc_manager.dart'; -import 'package:yumi/services/audio/rtm_manager.dart'; - -///聊天输入框 -class RoomMsgInput extends StatefulWidget { - String? atTextContent; - - RoomMsgInput({this.atTextContent}); - - @override - _RoomMsgInputState createState() => _RoomMsgInputState(); -} - -class _RoomMsgInputState extends State { - bool showSend = false; - - FocusNode msgNode = FocusNode(); - - TextEditingController controller = TextEditingController(); - - @override - void initState() { - super.initState(); - if (widget.atTextContent != null) { - controller.value = TextEditingValue(text: widget.atTextContent ?? ""); - } - } - - @override - Widget build(BuildContext context) { - // FocusScope.of(context).requestFocus(msgNode); - return Scaffold( - backgroundColor: Colors.transparent, - body: Column( - children: [ - Expanded( - child: GestureDetector( - onTap: () { - Navigator.pop(context); - }, - ), - ), - float(context), - ], - ), - ); - } - - Widget float(BuildContext context) { - return Container( - decoration: BoxDecoration(color: Colors.black), - padding: EdgeInsets.symmetric(horizontal: 25.w, vertical: 12.w), - height: 56.0, - child: Container( - decoration: BoxDecoration( - color: Colors.white24, - 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: true, - onSubmitted: (s) { - Navigator.pop(context); - if (controller.text.isNotEmpty) { - var currenRoom = - Provider.of( - context, - listen: false, - ).currenRoom; - if (currenRoom != null) { - Provider.of(context, listen: false).dispatchMessage( - Msg( - groupId: - currenRoom - .roomProfile - ?.roomProfile - ?.roomAccount ?? - "", - role: - Provider.of( - context, - listen: false, - ).currenRoom?.entrants?.roles ?? - "", - msg: controller.text, - type: SCRoomMsgType.text, - user: AccountStorage().getCurrentUser()?.userProfile, - ), - ); - } - } - }, - onChanged: (s) { - setState(() { - showSend = controller.text.isNotEmpty; - }); - }, - decoration: InputDecoration( - hintText: SCAppLocalizations.of(context)!.pleaseChatFfriendly, - fillColor: Colors.transparent, - hintStyle: TextStyle(color: Colors.white60, fontSize: sp(14)), - contentPadding: const EdgeInsets.symmetric(horizontal: 15), - 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( - onTap: () { - Navigator.pop(context); - if (controller.text.isNotEmpty) { - var currenRoom = - Provider.of( - context, - listen: false, - ).currenRoom; - if (currenRoom != null) { - Provider.of(context, listen: false).dispatchMessage( - Msg( - groupId: - currenRoom.roomProfile?.roomProfile?.roomAccount ?? - "", - role: - Provider.of( - context, - listen: false, - ).currenRoom?.entrants?.roles ?? - "", - msg: controller.text, - type: SCRoomMsgType.text, - user: AccountStorage().getCurrentUser()?.userProfile, - ), - addLocal: true, - ); - } - } - }, - 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), - ], - ), - ), - ); - } -} - -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; -} +import 'package:extended_text/extended_text.dart'; +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_emoji_datas.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/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_msg_item.dart'; + +///聊天输入框 +class RoomMsgInput extends StatefulWidget { + final String? atTextContent; + + const RoomMsgInput({super.key, this.atTextContent}); + + @override + State createState() => _RoomMsgInputState(); +} + +class _RoomMsgInputState extends State { + bool showSend = false; + bool showEmoji = false; + + final FocusNode msgNode = FocusNode(); + final TextEditingController controller = TextEditingController(); + + BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy; + Color get _panelBackgroundColor => Colors.white24; + + @override + void initState() { + super.initState(); + 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 + 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() + : _strategy.getSCMessageChatPageEmojiIcon(), + color: Colors.white, + 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: true, + 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() { + return Container( + height: 150.w, + decoration: BoxDecoration( + color: _panelBackgroundColor, + borderRadius: BorderRadius.circular(height(5)), + ), + child: GridView.builder( + padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 8, + crossAxisSpacing: 8.w, + mainAxisSpacing: 8.w, + ), + itemCount: SCEmojiDatas.smileys.length, + itemBuilder: (context, index) { + final emoji = SCEmojiDatas.smileys[index]; + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + _insertEmoji(emoji); + }, + child: Center( + child: ExtendedText(emoji, style: TextStyle(fontSize: sp(15))), + ), + ); + }, + ), + ); + } + + void _toggleEmojiPanel() { + if (showEmoji) { + setState(() { + showEmoji = false; + }); + FocusScope.of(context).requestFocus(msgNode); + return; + } + + SCKeybordUtil.hide(context); + setState(() { + showEmoji = true; + }); + } + + void _insertEmoji(String emoji) { + final value = controller.value; + final selection = value.selection; + final fallbackOffset = value.text.length; + final start = selection.isValid ? selection.start : fallbackOffset; + final end = selection.isValid ? selection.end : fallbackOffset; + final safeStart = start < 0 ? fallbackOffset : start; + final safeEnd = end < 0 ? fallbackOffset : end; + final nextText = value.text.replaceRange(safeStart, safeEnd, emoji); + + controller.value = value.copyWith( + text: nextText, + selection: TextSelection.collapsed(offset: safeStart + emoji.length), + composing: TextRange.empty, + ); + + setState(() { + showSend = controller.text.trim().isNotEmpty; + }); + } + + 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 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; +} diff --git a/需求进度.md b/需求进度.md index 0920941..144e088 100644 --- a/需求进度.md +++ b/需求进度.md @@ -30,6 +30,7 @@ - 已定位 `FOREGROUND_SERVICE_MEDIA_PROJECTION` 的来源为 Agora `full-screen-sharing` 依赖,并开始从主 Manifest 覆盖移除。 - 已移除 `FOREGROUND_SERVICE_MEDIA_PROJECTION` 相关声明并重新打出正式 `AAB`。 - 已按当前需求打出正式 `arm64-v8a` 单架构 `APK`。 +- 已将新的 `google-services.json` 替换进工程,准备重新打包验证。 ## 进行中模块 - 评估 iOS 正式签名参数接入后导出 `.ipa` 的流程。 @@ -54,6 +55,7 @@ - `pubspec.lock` - `lib/ui_kit/components/dialog/dialog.dart` - `android/app/src/main/AndroidManifest.xml` +- `android/app/google-services.json` - `ios/Runner/Info.plist` - `scripts/tinypng_batch.rb` - `scripts/build_release.py` @@ -160,6 +162,11 @@ - 本轮按需执行 `flutter build apk --release --split-per-abi --target-platform android-arm64 --split-debug-info=build/symbols/android` 已成功,产物为: - `build/app/outputs/flutter-apk/app-arm64-v8a-release.apk` - 当前 `arm64-v8a` APK 体积约 `128.7MB` +- 已核对新旧 `google-services.json`: +- 原文件与新文件都匹配包名 `com.org.yumi` +- 下载文件已成功覆盖到 `android/app/google-services.json` +- 当前工程内文件 `SHA256` 为: +- `a1706496a01f74d27e6c60598144cecc978934b02a87567e1ced887b5b0185d5` ## 已知问题 - `ios/Podfile.lock` 还保留旧插件记录,因为本轮未执行 `pod install`;但 Flutter 当前依赖链路已经不再包含已移除插件。