emoji添加

This commit is contained in:
NIGGER SLAYER 2026-04-14 14:12:22 +08:00
parent 88d9428970
commit e927bf4520
2 changed files with 323 additions and 212 deletions

View File

@ -1,212 +1,316 @@
import 'dart:ui' as ui; import 'package:extended_text/extended_text.dart';
import 'package:extended_text_field/extended_text_field.dart';
import 'package:extended_text_field/extended_text_field.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter/material.dart'; import 'package:provider/provider.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:yumi/app/config/business_logic_strategy.dart';
import 'package:yumi/app_localizations.dart'; import 'package:yumi/app/constants/sc_emoji_datas.dart';
import 'package:yumi/app/constants/sc_room_msg_type.dart'; import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/app/constants/sc_room_msg_type.dart';
import 'package:yumi/shared/business_logic/usecases/sc_case.dart'; import 'package:yumi/app/constants/sc_screen.dart';
import 'package:provider/provider.dart'; import 'package:yumi/app_localizations.dart';
import 'package:yumi/app/constants/sc_screen.dart'; import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/shared/business_logic/usecases/sc_case.dart';
import 'package:yumi/services/audio/rtm_manager.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 {
String? atTextContent; ///
class RoomMsgInput extends StatefulWidget {
RoomMsgInput({this.atTextContent}); final String? atTextContent;
@override const RoomMsgInput({super.key, this.atTextContent});
_RoomMsgInputState createState() => _RoomMsgInputState();
} @override
State<RoomMsgInput> createState() => _RoomMsgInputState();
class _RoomMsgInputState extends State<RoomMsgInput> { }
bool showSend = false;
class _RoomMsgInputState extends State<RoomMsgInput> {
FocusNode msgNode = FocusNode(); bool showSend = false;
bool showEmoji = false;
TextEditingController controller = TextEditingController();
final FocusNode msgNode = FocusNode();
@override final TextEditingController controller = TextEditingController();
void initState() {
super.initState(); BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy;
if (widget.atTextContent != null) { Color get _panelBackgroundColor => Colors.white24;
controller.value = TextEditingValue(text: widget.atTextContent ?? "");
} @override
} void initState() {
super.initState();
@override if (widget.atTextContent != null) {
Widget build(BuildContext context) { controller.value = TextEditingValue(text: widget.atTextContent ?? "");
// FocusScope.of(context).requestFocus(msgNode); showSend = controller.text.trim().isNotEmpty;
return Scaffold( }
backgroundColor: Colors.transparent, }
body: Column(
children: <Widget>[ @override
Expanded( void dispose() {
child: GestureDetector( controller.dispose();
onTap: () { msgNode.dispose();
Navigator.pop(context); super.dispose();
}, }
),
), @override
float(context), Widget build(BuildContext context) {
], final keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
), return Scaffold(
); backgroundColor: Colors.transparent,
} resizeToAvoidBottomInset: false,
body: Column(
Widget float(BuildContext context) { children: <Widget>[
return Container( Expanded(
decoration: BoxDecoration(color: Colors.black), child: GestureDetector(
padding: EdgeInsets.symmetric(horizontal: 25.w, vertical: 12.w), behavior: HitTestBehavior.opaque,
height: 56.0, onTap: () {
child: Container( Navigator.pop(context);
decoration: BoxDecoration( },
color: Colors.white24, ),
borderRadius: BorderRadius.circular(height(5)), ),
), AnimatedPadding(
child: Row( duration: const Duration(milliseconds: 220),
children: [ curve: Curves.easeOut,
Expanded( padding: EdgeInsets.only(bottom: keyboardHeight),
child: ExtendedTextField( child: _buildBottomPanel(context),
controller: controller, ),
textDirection: ],
widget.atTextContent != null ? TextDirection.ltr : null, ),
specialTextSpanBuilder: AtTextSpanBuilder(), );
focusNode: msgNode, }
autofocus: true,
onSubmitted: (s) { Widget _buildBottomPanel(BuildContext context) {
Navigator.pop(context); return Container(
if (controller.text.isNotEmpty) { color: Colors.black,
var currenRoom = child: SafeArea(
Provider.of<RtcProvider>( top: false,
context, child: Padding(
listen: false, padding: EdgeInsets.fromLTRB(25.w, 12.w, 25.w, 12.w),
).currenRoom; child: Column(
if (currenRoom != null) { mainAxisSize: MainAxisSize.min,
Provider.of<RtmProvider>(context, listen: false).dispatchMessage( children: [
Msg( Row(
groupId: crossAxisAlignment: CrossAxisAlignment.center,
currenRoom children: [
.roomProfile GestureDetector(
?.roomProfile behavior: HitTestBehavior.opaque,
?.roomAccount ?? onTap: _toggleEmojiPanel,
"", child: SizedBox(
role: width: 24.w,
Provider.of<RtcProvider>( height: 24.w,
context, child: Image.asset(
listen: false, showEmoji
).currenRoom?.entrants?.roles ?? ? _strategy.getSCMessageChatPageChatKeyboardIcon()
"", : _strategy.getSCMessageChatPageEmojiIcon(),
msg: controller.text, color: Colors.white,
type: SCRoomMsgType.text, fit: BoxFit.contain,
user: AccountStorage().getCurrentUser()?.userProfile, ),
), ),
); ),
} SizedBox(width: 15.w),
} Expanded(child: _buildInputBar(context)),
}, ],
onChanged: (s) { ),
setState(() { if (showEmoji) SizedBox(height: 12.w),
showSend = controller.text.isNotEmpty; if (showEmoji) _buildEmojiPanel(),
}); ],
}, ),
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: '', Widget _buildInputBar(BuildContext context) {
isDense: true, return Container(
filled: false, decoration: BoxDecoration(
focusColor: Colors.transparent, color: _panelBackgroundColor,
hoverColor: Colors.transparent, borderRadius: BorderRadius.circular(height(5)),
border: InputBorder.none, ),
enabledBorder: InputBorder.none, child: Row(
focusedBorder: InputBorder.none, children: [
disabledBorder: InputBorder.none, Expanded(
errorBorder: InputBorder.none, child: ExtendedTextField(
focusedErrorBorder: InputBorder.none, controller: controller,
), textDirection:
style: TextStyle( widget.atTextContent != null ? TextDirection.ltr : null,
fontSize: ScreenUtil().setSp(14), specialTextSpanBuilder: AtTextSpanBuilder(),
color: Colors.white, focusNode: msgNode,
), autofocus: true,
), textInputAction: TextInputAction.send,
), onTap: () {
GestureDetector( if (showEmoji) {
onTap: () { setState(() {
Navigator.pop(context); showEmoji = false;
if (controller.text.isNotEmpty) { });
var currenRoom = }
Provider.of<RtcProvider>( },
context, onSubmitted: (_) {
listen: false, _sendMessage();
).currenRoom; },
if (currenRoom != null) { onChanged: (s) {
Provider.of<RtmProvider>(context, listen: false).dispatchMessage( setState(() {
Msg( showSend = controller.text.trim().isNotEmpty;
groupId: });
currenRoom.roomProfile?.roomProfile?.roomAccount ?? },
"", decoration: InputDecoration(
role: hintText: SCAppLocalizations.of(context)!.pleaseChatFfriendly,
Provider.of<RtcProvider>( fillColor: Colors.transparent,
context, hintStyle: TextStyle(color: Colors.white60, fontSize: sp(14)),
listen: false, contentPadding: EdgeInsets.symmetric(horizontal: 15.w),
).currenRoom?.entrants?.roles ?? counterText: '',
"", isDense: true,
msg: controller.text, filled: false,
type: SCRoomMsgType.text, focusColor: Colors.transparent,
user: AccountStorage().getCurrentUser()?.userProfile, hoverColor: Colors.transparent,
), border: InputBorder.none,
addLocal: true, enabledBorder: InputBorder.none,
); focusedBorder: InputBorder.none,
} disabledBorder: InputBorder.none,
} errorBorder: InputBorder.none,
}, focusedErrorBorder: InputBorder.none,
child: Container( ),
padding: EdgeInsets.all(4.w), style: TextStyle(
width: 30.w, fontSize: ScreenUtil().setSp(14),
height: 30.w, color: Colors.white,
child: Image.asset("sc_images/room/sc_icon_room_message_send.png"), ),
), ),
), ),
SizedBox(width: 3.w), GestureDetector(
], behavior: HitTestBehavior.opaque,
), onTap: _sendMessage,
), child: Opacity(
); opacity: showSend ? 1 : 0.5,
} child: Container(
} padding: EdgeInsets.all(4.w),
width: 30.w,
class PopRoute extends PopupRoute { height: 30.w,
final Duration _duration = Duration(milliseconds: 350); child: Image.asset(
Widget child; "sc_images/room/sc_icon_room_message_send.png",
),
PopRoute({required this.child}); ),
),
@override ),
Color? get barrierColor => null; SizedBox(width: 3.w),
],
@override ),
bool get barrierDismissible => true; );
}
@override
String? get barrierLabel => null; Widget _buildEmojiPanel() {
return Container(
@override height: 150.w,
Widget buildPage( decoration: BoxDecoration(
BuildContext context, color: _panelBackgroundColor,
Animation<double> animation, borderRadius: BorderRadius.circular(height(5)),
Animation<double> secondaryAnimation, ),
) { child: GridView.builder(
return child; padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w),
} gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 8,
@override crossAxisSpacing: 8.w,
Duration get transitionDuration => _duration; 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<RtcProvider>(context, listen: false);
final currenRoom = rtcProvider.currenRoom;
if (currenRoom == null) {
return;
}
Provider.of<RtmProvider>(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<double> animation,
Animation<double> secondaryAnimation,
) {
return child;
}
@override
Duration get transitionDuration => _duration;
}

View File

@ -30,6 +30,7 @@
- 已定位 `FOREGROUND_SERVICE_MEDIA_PROJECTION` 的来源为 Agora `full-screen-sharing` 依赖,并开始从主 Manifest 覆盖移除。 - 已定位 `FOREGROUND_SERVICE_MEDIA_PROJECTION` 的来源为 Agora `full-screen-sharing` 依赖,并开始从主 Manifest 覆盖移除。
- 已移除 `FOREGROUND_SERVICE_MEDIA_PROJECTION` 相关声明并重新打出正式 `AAB` - 已移除 `FOREGROUND_SERVICE_MEDIA_PROJECTION` 相关声明并重新打出正式 `AAB`
- 已按当前需求打出正式 `arm64-v8a` 单架构 `APK` - 已按当前需求打出正式 `arm64-v8a` 单架构 `APK`
- 已将新的 `google-services.json` 替换进工程,准备重新打包验证。
## 进行中模块 ## 进行中模块
- 评估 iOS 正式签名参数接入后导出 `.ipa` 的流程。 - 评估 iOS 正式签名参数接入后导出 `.ipa` 的流程。
@ -54,6 +55,7 @@
- `pubspec.lock` - `pubspec.lock`
- `lib/ui_kit/components/dialog/dialog.dart` - `lib/ui_kit/components/dialog/dialog.dart`
- `android/app/src/main/AndroidManifest.xml` - `android/app/src/main/AndroidManifest.xml`
- `android/app/google-services.json`
- `ios/Runner/Info.plist` - `ios/Runner/Info.plist`
- `scripts/tinypng_batch.rb` - `scripts/tinypng_batch.rb`
- `scripts/build_release.py` - `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` 已成功,产物为: - 本轮按需执行 `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` - `build/app/outputs/flutter-apk/app-arm64-v8a-release.apk`
- 当前 `arm64-v8a` APK 体积约 `128.7MB` - 当前 `arm64-v8a` APK 体积约 `128.7MB`
- 已核对新旧 `google-services.json`
- 原文件与新文件都匹配包名 `com.org.yumi`
- 下载文件已成功覆盖到 `android/app/google-services.json`
- 当前工程内文件 `SHA256` 为:
- `a1706496a01f74d27e6c60598144cecc978934b02a87567e1ced887b5b0185d5`
## 已知问题 ## 已知问题
- `ios/Podfile.lock` 还保留旧插件记录,因为本轮未执行 `pod install`;但 Flutter 当前依赖链路已经不再包含已移除插件。 - `ios/Podfile.lock` 还保留旧插件记录,因为本轮未执行 `pod install`;但 Flutter 当前依赖链路已经不再包含已移除插件。