Browse Source

合并代码

master
王子贤 2 months ago
parent
commit
77dabb01b3
17 changed files with 495 additions and 245 deletions
  1. 11
      lib/controller/discover/room_controller.dart
  2. 85
      lib/controller/message/call_controller.dart
  3. 6
      lib/model/mine/rose_data.dart
  4. 3
      lib/network/api_urls.dart
  5. 6
      lib/network/rtc_api.dart
  6. 34
      lib/network/rtc_api.g.dart
  7. 285
      lib/pages/discover/live_room_page.dart
  8. 21
      lib/pages/message/chat_page.dart
  9. 2
      lib/pages/mine/rose_page.dart
  10. 2
      lib/widget/live/live_recharge_popup.dart
  11. 9
      lib/widget/live/live_room_anchor_showcase.dart
  12. 4
      lib/widget/live/live_room_notice_chat_panel.dart
  13. 2
      lib/widget/live/live_room_pay_item.dart
  14. 4
      lib/widget/live/live_room_user_header.dart
  15. 11
      lib/widget/message/call_type_selection_dialog.dart
  16. 253
      lib/widget/message/chat_input_bar.dart
  17. 2
      pubspec.yaml

11
lib/controller/discover/room_controller.dart

@ -59,6 +59,7 @@ class RoomController extends GetxController with WidgetsBindingObserver {
///
final RxInt roseCount = 0.obs;
var isDialogShowing = false.obs;
///
final LiveChatMessageService _messageService =
@ -94,6 +95,10 @@ class RoomController extends GetxController with WidgetsBindingObserver {
chatMessages.clear();
}
void setDialogDismiss(bool flag){
isDialogShowing.value = flag;
}
///
void _registerMessageListener() {
_messageService.registerMessageListener(
@ -460,8 +465,12 @@ class RoomController extends GetxController with WidgetsBindingObserver {
Get.log('❌ 送礼失败: ${response.data.data}');
// 使 addPostFrameCallback toast
WidgetsBinding.instance.addPostFrameCallback((_) {
setDialogDismiss(true);
SmartDialog.show(
alignment: Alignment.bottomCenter,
onDismiss: (){
setDialogDismiss(false);
},
maskColor: Colors.black.withOpacity(0.5),
builder: (_) => const LiveRechargePopup(),
);
@ -671,10 +680,8 @@ class RoomController extends GetxController with WidgetsBindingObserver {
print('关闭小窗口失败: $e');
}
}
//
await leaveChannel();
//
Get.off(
() => LiveEndPage(isKickedOut: true, operatorName: operatorName),

85
lib/controller/message/call_controller.dart

@ -97,6 +97,9 @@ class CallController extends GetxController {
// 30
Timer? _callTimeoutTimer;
// 1
Timer? _consumeTimer;
// UID
final Rxn<int> remoteUid = Rxn<int>();
@ -165,6 +168,7 @@ class CallController extends GetxController {
if (!response.data.data!.success && response.data.data!.code == 'E0002') {
// toast
SmartDialog.showToast('玫瑰不足请充值');
isCreatingChannel.value = false;
Get.log('❌ 送礼失败: ${response.data.data}');
// 使 addPostFrameCallback toast
WidgetsBinding.instance.addPostFrameCallback((_) {
@ -256,15 +260,17 @@ class CallController extends GetxController {
type: type,
toUserId: targetUserId,
);
_callUid = channelData?.uid;
_callChannelId = channelData?.channelId;
if (channelData == null) {
if (channelData == null) {
return false;
}
if (!channelData.success) {
print('❌ [CallController] 创建RTC频道失败,无法发起通话');
SmartDialog.showToast('创建通话频道失败');
return false;
}
print('✅ [CallController] RTC频道创建成功: ${channelData.channelId}');
_callUid = channelData?.uid;
_callChannelId = channelData?.channelId;
print('✅ [CallController] RTC频道创建成功: ${channelData!.channelId}');
//
final session = CallSession(
@ -440,6 +446,16 @@ class CallController extends GetxController {
'📞 [CallController] 从消息中获取到发起方 UID: $initiatorUid,已设置 remoteUid',
);
}
//
// _callChannelId
if (_callChannelId != null && _callChannelId!.isNotEmpty) {
Future .delayed(Duration(seconds: 1), () async {
await _consumeOneOnOneRtcChannel();
});
_startConsumeTimer();
print('✅ [CallController] 接收方接听后已启动消费定时器');
}
} else {
SmartDialog.showToast('获取RTC token失败');
return false;
@ -505,6 +521,7 @@ class CallController extends GetxController {
//
_stopCallTimer();
_stopCallTimeoutTimer();
_stopConsumeTimer();
// UID
currentCall.value = null;
@ -561,6 +578,43 @@ class CallController extends GetxController {
_callTimeoutTimer = null;
}
/// 1
void _startConsumeTimer() {
_stopConsumeTimer(); //
_consumeTimer = Timer.periodic(Duration(minutes: 1), (timer) {
_consumeOneOnOneRtcChannel();
});
print('✅ [CallController] 已启动消费定时器,每隔1分钟调用一次');
}
///
void _stopConsumeTimer() {
_consumeTimer?.cancel();
_consumeTimer = null;
}
/// RTC频道接口
Future<void> _consumeOneOnOneRtcChannel() async {
final consumeChannelId = _callChannelId;
if (consumeChannelId == null || consumeChannelId.isEmpty) {
print('⚠️ [CallController] channelId为空,无法调用消费接口');
return;
}
try {
final response = await _networkService.rtcApi.consumeOneOnOneRtcChannel({
'channelId': consumeChannelId,
});
if (response.data.isSuccess) {
print('✅ [CallController] 已调用消费一对一RTC频道接口,channelId: $consumeChannelId');
} else {
print('⚠️ [CallController] 消费一对一RTC频道接口失败: ${response.data.message}');
}
} catch (e) {
print('⚠️ [CallController] 调用消费接口异常: $e');
}
}
///
Future<bool> _sendCallMessage({
required String targetUserId,
@ -858,6 +912,9 @@ class CallController extends GetxController {
//
_stopCallTimer();
//
_stopConsumeTimer();
// RTC频道
await RTCManager.instance.leaveChannel();
@ -904,6 +961,23 @@ class CallController extends GetxController {
//
print('📞 [CallController] 通话已接通,callStatus=$callStatus');
// _callChannelId 使 channelId
if (channelId != null && channelId.isNotEmpty) {
_callChannelId = channelId;
}
// 1
//
if (_consumeTimer == null) {
_consumeOneOnOneRtcChannel();
_startConsumeTimer();
print('✅ [CallController] 通话接通后已启动消费定时器');
} else {
//
_consumeOneOnOneRtcChannel();
print('✅ [CallController] 消费定时器已存在,只调用一次消费接口');
}
// UID并启动计时器
if (callSession.isInitiator) {
//
@ -971,6 +1045,7 @@ class CallController extends GetxController {
isSpeakerOn.value = false;
_callChannelId = null;
_callUid = null;
_stopConsumeTimer();
_callAudioPlayer.dispose();
super.onClose();
}

6
lib/model/mine/rose_data.dart

@ -11,6 +11,7 @@ class RoseData {
String? purchaseTimeValue;
String? validityPeriodDays;
String? liveDurationHours;
String? unitSellingPriceStr;
RoseData(
{this.productId,
@ -24,7 +25,8 @@ class RoseData {
this.unitSellingPrice,
this.purchaseTimeValue,
this.validityPeriodDays,
this.liveDurationHours
this.liveDurationHours,
this.unitSellingPriceStr,
});
RoseData.fromJson(Map<String, dynamic> json) {
@ -40,6 +42,7 @@ class RoseData {
purchaseTimeValue = json['purchaseTimeValue'];
validityPeriodDays = json['validityPeriodDays'];
liveDurationHours = json['liveDurationHours'];
unitSellingPriceStr = json['unitSellingPriceStr'];
}
Map<String, dynamic> toJson() {
@ -56,6 +59,7 @@ class RoseData {
data['purchaseTimeValue'] = this.purchaseTimeValue;
data['validityPeriodDays'] = this.validityPeriodDays;
data['liveDurationHours'] = this.liveDurationHours;
data['unitSellingPriceStr'] = this.unitSellingPriceStr;
return data;
}
}

3
lib/network/api_urls.dart

@ -149,4 +149,7 @@ class ApiUrls {
static const String listChatAudioProduct =
'dating-agency-chat-audio/user/list/chat-audio-product';
static const String consumeOneOnOneRtcChannel =
'dating-agency-chat-audio/user/consume/one-on-one/rtc-channel';
}

6
lib/network/rtc_api.dart

@ -130,4 +130,10 @@ abstract class RtcApi {
Future<HttpResponse<BaseResponse<List<ChatAudioProductModel>>>> listChatAudioProduct(
@Query('toUserId') String toUserId,
);
/// RTC频道
@POST(ApiUrls.consumeOneOnOneRtcChannel)
Future<HttpResponse<BaseResponse<dynamic>>> consumeOneOnOneRtcChannel(
@Body() Map<String, dynamic> data,
);
}

34
lib/network/rtc_api.g.dart

@ -749,6 +749,40 @@ class _RtcApi implements RtcApi {
return httpResponse;
}
@override
Future<HttpResponse<BaseResponse<dynamic>>> consumeOneOnOneRtcChannel(
Map<String, dynamic> data,
) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
_data.addAll(data);
final _options = _setStreamType<HttpResponse<BaseResponse<dynamic>>>(
Options(method: 'POST', headers: _headers, extra: _extra)
.compose(
_dio.options,
'dating-agency-chat-audio/user/consume/one-on-one/rtc-channel',
queryParameters: queryParameters,
data: _data,
)
.copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)),
);
final _result = await _dio.fetch<Map<String, dynamic>>(_options);
late BaseResponse<dynamic> _value;
try {
_value = BaseResponse<dynamic>.fromJson(
_result.data!,
(json) => json as dynamic,
);
} on Object catch (e, s) {
errorLogger?.logError(e, s, _options);
rethrow;
}
final httpResponse = HttpResponse(_value, _result);
return httpResponse;
}
RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
if (T != dynamic &&
!(requestOptions.responseType == ResponseType.bytes ||

285
lib/pages/discover/live_room_page.dart

@ -107,6 +107,7 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
_messageController.dispose();
_inputDialogController.dispose();
_inputDialogFocusNode.dispose();
SmartDialog.dismiss();
// 退RTM消息
if (Get.isRegistered<RoomController>()) {
final roomController = Get.find<RoomController>();
@ -183,9 +184,14 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
FocusScope.of(context).unfocus();
//
await _roomController.getVirtualAccount();
_roomController.setDialogDismiss(true);
SmartDialog.show(
backType: SmartBackType.block,
alignment: Alignment.bottomCenter,
maskColor: TDTheme.of(context).fontGyColor2,
onDismiss: () {
_roomController.setDialogDismiss(false);
},
builder: (_) => Obx(() {
// 使 API giftProducts使
final giftProducts = _roomController.giftProducts;
@ -204,9 +210,13 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
void _showRechargePopup() {
//
FocusScope.of(context).unfocus();
_roomController.setDialogDismiss(true);
SmartDialog.show(
alignment: Alignment.bottomCenter,
maskColor: TDTheme.of(context).fontGyColor2,
onDismiss: (){
_roomController.setDialogDismiss(false);
},
builder: (_) => const LiveRechargePopup(),
);
}
@ -233,151 +243,164 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
});
}
});
return Obx(() {
return PopScope(
canPop: !_roomController.isDialogShowing.value,
onPopInvokedWithResult: (bool didPop, Object? result) async {
// SmartDialog.dismiss();
// print('256>22>>' + didPop.toString());
// if (didPop) return;
return PopScope(
onPopInvokedWithResult: (bool didPop, Object? result) async {
SmartDialog.dismiss();
// 退RTM消息
if (Get.isRegistered<RoomController>()) {
final roomController = Get.find<RoomController>();
roomController.chatMessages.clear();
}
// pop Get.back()
if (!didPop) {
Get.back();
}
//
Future.delayed(const Duration(milliseconds: 200), () {
_overlayController.show();
});
},
child: Scaffold(
resizeToAvoidBottomInset: false,
body: Stack(
children: [
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color.fromRGBO(248, 242, 255, 1),
Color.fromRGBO(247, 247, 247, 1),
],
//
if (_roomController.isDialogShowing.value) {
SmartDialog.dismiss();
return; //
}
// 退RTM消息
if (Get.isRegistered<RoomController>()) {
final roomController = Get.find<RoomController>();
roomController.chatMessages.clear();
}
// pop Get.back()
if (!didPop) {
Get.back();
}
//
Future.delayed(const Duration(milliseconds: 200), () {
_overlayController.show();
});
},
child: Scaffold(
resizeToAvoidBottomInset: false,
body: Stack(
children: [
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color.fromRGBO(248, 242, 255, 1),
Color.fromRGBO(247, 247, 247, 1),
],
),
),
),
),
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Color.fromRGBO(19, 16, 47, 1),
Color.fromRGBO(19, 16, 47, 1),
],
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Color.fromRGBO(19, 16, 47, 1),
Color.fromRGBO(19, 16, 47, 1),
],
),
),
),
),
Container(
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
child: Column(
children: [
SizedBox(height: 10.w),
Obx(() {
final detail = _roomController.rtcChannelDetail.value;
final anchorInfo = detail?.anchorInfo;
Container(
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top,
),
child: Column(
children: [
SizedBox(height: 10.w),
Obx(() {
final detail = _roomController.rtcChannelDetail.value;
final anchorInfo = detail?.anchorInfo;
final userName = anchorInfo?.nickName ?? '用户';
final avatarAsset = anchorInfo?.profilePhoto ?? Assets.imagesUserAvatar;
const popularityText = '0'; // TODO: 使
final userName = anchorInfo?.nickName ?? '用户';
final avatarAsset =
anchorInfo?.profilePhoto ?? Assets.imagesUserAvatar;
const popularityText = '0'; // TODO: 使
return LiveRoomUserHeader(
userName: userName,
popularityText: popularityText,
avatarAsset: avatarAsset,
onCloseTap: () {
SmartDialog.dismiss();
// 退RTM消息
if (Get.isRegistered<RoomController>()) {
final roomController = Get.find<RoomController>();
roomController.chatMessages.clear();
}
Get.back();
//
Future.delayed(const Duration(milliseconds: 200), () {
_overlayController.show();
});
},
);
}),
SizedBox(height: 7.w),
LiveRoomAnchorShowcase(),
SizedBox(height: 5.w),
const LiveRoomActiveSpeaker(),
SizedBox(height: 9.w),
Expanded(child: const LiveRoomNoticeChatPanel()),
// / LiveRoomActionBar
if (MediaQuery.of(context).viewInsets.bottom == 0)
SafeArea(
top: false,
child: Padding(
padding: EdgeInsets.only(
bottom: 10.w,
left: 0,
right: 0,
),
child: LiveRoomActionBar(
messageController: _messageController,
onMessageChanged: (value) {
message = value;
},
onSendTap: _sendMessage,
onGiftTap: _showGiftPopup,
onChargeTap: _showRechargePopup,
onInputTap: _openInputDialog,
return LiveRoomUserHeader(
userName: userName,
popularityText: popularityText,
avatarAsset: avatarAsset,
onCloseTap: () {
SmartDialog.dismiss();
// 退RTM消息
if (Get.isRegistered<RoomController>()) {
final roomController = Get.find<RoomController>();
roomController.chatMessages.clear();
}
Get.back();
//
Future.delayed(const Duration(milliseconds: 200), () {
_overlayController.show();
});
},
);
}),
SizedBox(height: 7.w),
LiveRoomAnchorShowcase(),
SizedBox(height: 5.w),
const LiveRoomActiveSpeaker(),
SizedBox(height: 9.w),
Expanded(child: const LiveRoomNoticeChatPanel()),
// / LiveRoomActionBar
if (MediaQuery.of(context).viewInsets.bottom == 0)
SafeArea(
top: false,
child: Padding(
padding: EdgeInsets.only(
bottom: 10.w,
left: 0,
right: 0,
),
child: LiveRoomActionBar(
messageController: _messageController,
onMessageChanged: (value) {
message = value;
},
onSendTap: _sendMessage,
onGiftTap: _showGiftPopup,
onChargeTap: _showRechargePopup,
onInputTap: _openInputDialog,
),
),
),
),
],
),
),
// SVGA
const SvgaPlayerWidget(),
//
if (_showInputDialog) ...[
//
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
//
FocusScope.of(context).unfocus();
//
_hideInputDialog();
},
child: Container(color: Colors.transparent),
],
),
),
//
AnimatedPositioned(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
left: 0,
right: 0,
bottom: MediaQuery.of(context).viewInsets.bottom,
child: _InputDialogWidget(
controller: _inputDialogController,
focusNode: _inputDialogFocusNode,
onSend: _sendInputDialogMessage,
onClose: _hideInputDialog,
// SVGA
const SvgaPlayerWidget(),
//
if (_showInputDialog) ...[
//
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
//
FocusScope.of(context).unfocus();
//
_hideInputDialog();
},
child: Container(color: Colors.transparent),
),
),
),
//
AnimatedPositioned(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
left: 0,
right: 0,
bottom: MediaQuery.of(context).viewInsets.bottom,
child: _InputDialogWidget(
controller: _inputDialogController,
focusNode: _inputDialogFocusNode,
onSend: _sendInputDialogMessage,
onClose: _hideInputDialog,
),
),
],
],
],
),
),
),
);
);
});
}
}

21
lib/pages/message/chat_page.dart

@ -35,6 +35,25 @@ class ChatPage extends StatefulWidget {
}
class _ChatPageState extends State<ChatPage> {
///
String _calculateAgeFromBirthYear(String birthYear) {
if (birthYear.isEmpty) {
return '0';
}
try {
final year = int.tryParse(birthYear);
if (year == null) {
return '0';
}
final currentYear = DateTime.now().year;
final age = currentYear - year;
return age > 0 ? age.toString() : '0';
} catch (e) {
return '0';
}
}
final ScrollController _scrollController = ScrollController();
bool _isLoadingMore = false;
late ChatController _controller;
@ -515,7 +534,7 @@ class _ChatPageState extends State<ChatPage> {
),
SizedBox(width: 4.w),
Text(
'${marriageData.age}',
_calculateAgeFromBirthYear(marriageData.birthYear),
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[700],

2
lib/pages/mine/rose_page.dart

@ -289,7 +289,7 @@ class _PayItemState extends State<PayItem> {
),
),
Text(
"${widget.item.unitSellingPrice!.toStringAsFixed(2)}",
"${widget.item.unitSellingPriceStr}",
style: TextStyle(
fontSize: 11.w,
color: const Color.fromRGBO(144, 144, 144, 144),

2
lib/widget/live/live_recharge_popup.dart

@ -109,7 +109,7 @@ class LiveRechargePopup extends StatelessWidget {
final payList = roseList.map((rose) {
return {
'num': rose.purchaseTimeValue ?? 0,
'price': rose.unitSellingPrice ?? 0,
'price': rose.unitSellingPriceStr ?? "0",
'hasTag': rose.detailDesc != null && rose.detailDesc!.isNotEmpty,
'tagText': rose.detailDesc ?? '',
};

9
lib/widget/live/live_room_anchor_showcase.dart

@ -376,9 +376,13 @@ class _LiveRoomAnchorShowcaseState extends State<LiveRoomAnchorShowcase> {
}
void _showGuestListDialog(BuildContext context, bool isMaleSeat) {
_roomController.setDialogDismiss(true);
SmartDialog.show(
alignment: Alignment.bottomCenter,
maskColor: Colors.black.withOpacity(0.5),
onDismiss: () {
_roomController.setDialogDismiss(false);
},
builder: (context) {
return LiveRoomGuestListDialog(
initialTab: isMaleSeat ? 1 : 0, // 0: , 1:
@ -406,10 +410,13 @@ class _LiveRoomAnchorShowcaseState extends State<LiveRoomAnchorShowcase> {
// ValueNotifier
final activeGift = ValueNotifier<int?>(null);
final giftNum = ValueNotifier<int>(1);
_roomController.setDialogDismiss(true);
SmartDialog.show(
alignment: Alignment.bottomCenter,
maskColor: Colors.black.withOpacity(0.5),
onDismiss: (){
_roomController.setDialogDismiss(false);
},
builder: (context) {
return Obx(() {
//

4
lib/widget/live/live_room_notice_chat_panel.dart

@ -180,9 +180,13 @@ class _LiveRoomNoticeChatPanelState extends State<LiveRoomNoticeChatPanel> {
final cardNum = linkMicCard?.num ?? 0;
// "上麦20玫瑰"20
if (cardNum == 0 && controller.roseCount.value < 20) {
controller.setDialogDismiss(true);
SmartDialog.show(
alignment: Alignment.bottomCenter,
maskColor: Colors.black.withOpacity(0.5),
onDismiss: (){
controller.setDialogDismiss(false);
},
builder: (_) => const LiveRechargePopup(),
);
return;

2
lib/widget/live/live_room_pay_item.dart

@ -70,7 +70,7 @@ class _LiveRoomPayItemState extends State<LiveRoomPayItem> {
),
),
Text(
"${(widget.item["price"])?.toStringAsFixed(2) ?? '0.00'}",
"${(widget.item["price"])}",
style: TextStyle(
fontSize: 11.w,
color: const Color.fromRGBO(144, 144, 144, 1),

4
lib/widget/live/live_room_user_header.dart

@ -84,8 +84,12 @@ class LiveRoomUserHeader extends StatelessWidget {
if (isHost)
GestureDetector(
onTap: () {
roomController.setDialogDismiss(true);
SmartDialog.showAttach(
targetContext: context,
onDismiss: (){
roomController.setDialogDismiss(false);
},
builder: (context) {
//
final hasGuests =

11
lib/widget/message/call_type_selection_dialog.dart

@ -38,11 +38,18 @@ class CallTypeSelectionDialog extends StatelessWidget {
///
String _formatPrice(ChatAudioProductModel? product) {
if (product == null) return '35玫瑰/分钟';
//
if (product.isFreeProduct) {
return '';
}
return '${product.unitSellingPrice.toInt()}玫瑰/分钟';
}
@override
Widget build(BuildContext context) {
String price = _formatPrice(voiceProduct);
return Container(
decoration: BoxDecoration(
color: Colors.white,
@ -65,7 +72,7 @@ class CallTypeSelectionDialog extends StatelessWidget {
padding: EdgeInsets.symmetric(vertical: 16.w),
child: Center(
child: Text(
'语音通话 (${_formatPrice(voiceProduct)})',
price.isNotEmpty ? '语音通话 ($price)' : '语音通话',
style: TextStyle(
fontSize: 16.sp,
color: const Color.fromRGBO(51, 51, 51, 1),
@ -91,7 +98,7 @@ class CallTypeSelectionDialog extends StatelessWidget {
padding: EdgeInsets.symmetric(vertical: 16.w),
child: Center(
child: Text(
'视频通话 (${_formatPrice(videoProduct)})',
price.isNotEmpty ? '视频通话 ($price)' : '视频通话',
style: TextStyle(
fontSize: 16.sp,
color: const Color.fromRGBO(51, 51, 51, 1),

253
lib/widget/message/chat_input_bar.dart

@ -1,6 +1,8 @@
import 'package:dating_touchme_app/extension/ex_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:extended_text_field/extended_text_field.dart';
import '../../generated/assets.dart';
import '../../config/emoji_config.dart';
@ -8,6 +10,135 @@ import '../emoji_panel.dart';
import 'more_options_view.dart';
import 'voice_input_view.dart';
/// - ExtendedTextField
class EmojiSpecialTextSpanBuilder extends SpecialTextSpanBuilder {
@override
SpecialText? createSpecialText(
String flag, {
TextStyle? textStyle,
SpecialTextGestureTapCallback? onTap,
required int index,
}) {
// [emoji:xxx]
if (flag.startsWith('[emoji:')) {
return EmojiSpecialText(
textStyle: textStyle,
onTap: onTap,
);
}
return null;
}
}
///
class EmojiSpecialText extends SpecialText {
EmojiSpecialText({
TextStyle? textStyle,
SpecialTextGestureTapCallback? onTap,
}) : super(
'[emoji:',
']',
textStyle,
onTap: onTap,
);
@override
InlineSpan finishText() {
// ID
final emojiId = toString().replaceAll('[emoji:', '').replaceAll(']', '');
final emoji = EmojiConfig.getEmojiById(emojiId);
if (emoji != null) {
// WidgetSpan
return WidgetSpan(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 2.w),
child: Image.asset(
emoji.path,
width: 24.w,
height: 24.w,
fit: BoxFit.contain,
),
),
);
}
//
return TextSpan(
text: toString(),
style: textStyle,
);
}
}
/// -
class EmojiTextInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
//
if (newValue.text.length < oldValue.text.length) {
final oldSelection = oldValue.selection;
final cursorOffset = oldSelection.baseOffset;
final deletedLength = oldValue.text.length - newValue.text.length;
//
final emojiRegex = RegExp(r'\[emoji:\d+\]');
final allMatches = emojiRegex.allMatches(oldValue.text);
//
for (final match in allMatches) {
final emojiStart = match.start;
final emojiEnd = match.end;
//
if (cursorOffset > emojiStart && cursorOffset <= emojiEnd) {
//
final beforeEmoji = oldValue.text.substring(0, emojiStart);
final afterEmoji = oldValue.text.substring(emojiEnd);
final newText = beforeEmoji + afterEmoji;
//
//
// = -
final emojiLength = emojiEnd - emojiStart;
int newCursorOffset = (cursorOffset - emojiLength).clamp(0, newText.length);
return TextEditingValue(
text: newText,
selection: TextSelection.collapsed(offset: newCursorOffset),
);
}
//
if (cursorOffset == emojiStart) {
//
if (cursorOffset + deletedLength <= emojiEnd) {
//
final beforeEmoji = oldValue.text.substring(0, emojiStart);
final afterEmoji = oldValue.text.substring(emojiEnd);
final newText = beforeEmoji + afterEmoji;
//
// = -
final emojiLength = emojiEnd - emojiStart;
int newCursorOffset = (cursorOffset - emojiLength).clamp(0, newText.length);
return TextEditingValue(
text: newText,
selection: TextSelection.collapsed(offset: newCursorOffset),
);
}
}
}
}
return newValue;
}
}
class ChatInputBar extends StatefulWidget {
final ValueChanged<String> onSendMessage;
final ValueChanged<List<String>>? onImageSelected;
@ -129,71 +260,15 @@ class _ChatInputBarState extends State<ChatInputBar> {
//
final currentText = _textController.text;
final emojiText = '[emoji:${emoji.id}]';
_textController.text = currentText + emojiText;
final newText = currentText + emojiText;
_textController.text = newText;
//
_textController.selection = TextSelection.fromPosition(
TextPosition(offset: _textController.text.length),
TextPosition(offset: newText.length),
);
setState(() {}); //
}
/// +
List<Widget> _buildInputContentWidgets() {
final List<Widget> widgets = [];
final text = _textController.text;
final RegExp emojiRegex = RegExp(r'\[emoji:(\d+)\]');
int lastMatchEnd = 0;
final matches = emojiRegex.allMatches(text);
for (final match in matches) {
//
if (match.start > lastMatchEnd) {
final textPart = text.substring(lastMatchEnd, match.start);
widgets.add(
Text(
textPart,
style: TextStyle(fontSize: 14.sp, color: Colors.black),
),
);
}
//
final emojiId = match.group(1);
if (emojiId != null) {
final emoji = EmojiConfig.getEmojiById(emojiId);
if (emoji != null) {
widgets.add(
Padding(
padding: EdgeInsets.symmetric(horizontal: 2.w),
child: Image.asset(
emoji.path,
width: 24.w,
height: 24.w,
fit: BoxFit.contain,
),
),
);
}
}
lastMatchEnd = match.end;
}
//
if (lastMatchEnd < text.length) {
final textPart = text.substring(lastMatchEnd);
widgets.add(
Text(
textPart,
style: TextStyle(fontSize: 14.sp, color: Colors.black),
),
);
}
return widgets;
}
@override
Widget build(BuildContext context) {
return Column(
@ -215,46 +290,28 @@ class _ChatInputBarState extends State<ChatInputBar> {
borderRadius: BorderRadius.circular(5.h),
),
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Stack(
children: [
//
TextField(
controller: _textController,
focusNode: _focusNode,
decoration: InputDecoration(
border: InputBorder.none,
hintText: "请输入聊天内容~",
hintStyle: TextStyle(
fontSize: 14.sp,
color: Colors.grey,
),
),
style: TextStyle(
fontSize: 14.sp,
color: _textController.text.contains('[emoji:')
? Colors.transparent
: Colors.black,
),
onChanged: (value) {
setState(() {}); //
},
),
//
if (_textController.text.contains('[emoji:'))
Positioned.fill(
child: IgnorePointer(
child: Align(
alignment: Alignment.centerLeft,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: _buildInputContentWidgets(),
),
),
),
),
),
child: ExtendedTextField(
controller: _textController,
focusNode: _focusNode,
specialTextSpanBuilder: EmojiSpecialTextSpanBuilder(),
inputFormatters: [
EmojiTextInputFormatter(),
],
decoration: InputDecoration(
border: InputBorder.none,
hintText: "请输入聊天内容~",
hintStyle: TextStyle(
fontSize: 14.sp,
color: Colors.grey,
),
),
style: TextStyle(
fontSize: 14.sp,
color: Colors.black,
),
onChanged: (value) {
setState(() {}); //
},
),
),
),

2
pubspec.yaml

@ -81,6 +81,7 @@ dependencies:
ota_update: ^7.1.0
flutter_local_notifications: ^19.5.0
app_badge_plus: ^1.2.6
extended_text_field: ^16.0.2
dev_dependencies:
flutter_test:
@ -148,4 +149,3 @@ flutter_launcher_icons:
image_path: "assets/images/app_logo.jpg"
min_sdk_android: 21
remove_alpha_ios: true
Loading…
Cancel
Save