2025-07-21 15:46:30 +08:00
|
|
|
|
/// 聊天模板
|
|
|
|
|
library;
|
|
|
|
|
|
2025-08-21 10:50:38 +08:00
|
|
|
|
import 'dart:convert';
|
2025-09-09 10:57:52 +08:00
|
|
|
|
import 'dart:io';
|
2025-08-21 10:50:38 +08:00
|
|
|
|
|
2025-07-21 15:46:30 +08:00
|
|
|
|
import 'package:flutter/material.dart';
|
2025-08-21 10:50:38 +08:00
|
|
|
|
import 'package:flutter/services.dart';
|
2025-07-21 15:46:30 +08:00
|
|
|
|
import 'package:get/get.dart';
|
2025-09-09 10:57:52 +08:00
|
|
|
|
import 'package:image_picker/image_picker.dart';
|
2025-07-21 15:46:30 +08:00
|
|
|
|
import 'package:loopin/IM/controller/chat_detail_controller.dart';
|
|
|
|
|
import 'package:loopin/IM/im_message.dart';
|
2025-08-21 10:50:38 +08:00
|
|
|
|
import 'package:loopin/IM/im_result.dart';
|
2025-07-21 15:46:30 +08:00
|
|
|
|
import 'package:loopin/IM/im_service.dart';
|
2025-09-09 10:57:52 +08:00
|
|
|
|
import 'package:loopin/components/image_viewer.dart';
|
2025-08-21 10:50:38 +08:00
|
|
|
|
import 'package:loopin/components/network_or_asset_image.dart';
|
|
|
|
|
import 'package:loopin/components/preview_video.dart';
|
|
|
|
|
import 'package:loopin/models/summary_type.dart';
|
|
|
|
|
import 'package:loopin/utils/audio_player_service.dart';
|
|
|
|
|
import 'package:loopin/utils/snapshot.dart';
|
|
|
|
|
import 'package:loopin/utils/voice_service.dart';
|
2025-09-09 10:57:52 +08:00
|
|
|
|
import 'package:mime/mime.dart';
|
2025-07-21 15:46:30 +08:00
|
|
|
|
import 'package:shirne_dialog/shirne_dialog.dart';
|
|
|
|
|
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
|
|
|
|
|
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
|
2025-09-09 10:57:52 +08:00
|
|
|
|
import 'package:video_player/video_player.dart';
|
2025-08-21 10:50:38 +08:00
|
|
|
|
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
|
2025-07-21 15:46:30 +08:00
|
|
|
|
|
|
|
|
|
import '../../styles/index.dart';
|
|
|
|
|
import '../../utils/index.dart';
|
|
|
|
|
import './components/redpacket.dart';
|
|
|
|
|
import './components/richtext.dart';
|
|
|
|
|
// import 'mock/chat_json.dart';
|
|
|
|
|
import 'mock/emoj_json.dart';
|
|
|
|
|
|
|
|
|
|
class Chat extends StatefulWidget {
|
|
|
|
|
const Chat({super.key});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<Chat> createState() => _ChatState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _ChatState extends State<Chat> with SingleTickerProviderStateMixin {
|
2025-09-09 10:57:52 +08:00
|
|
|
|
final ImagePicker _picker = ImagePicker();
|
|
|
|
|
|
2025-07-21 15:46:30 +08:00
|
|
|
|
late final ChatDetailController controller;
|
|
|
|
|
// 接收参数
|
2025-08-21 10:50:38 +08:00
|
|
|
|
late final Rx<V2TimConversation> arguments;
|
2025-07-21 15:46:30 +08:00
|
|
|
|
|
|
|
|
|
late String selfUserId;
|
|
|
|
|
|
|
|
|
|
// 聊天消息模块
|
|
|
|
|
final bool isNeedScrollBottom = true;
|
|
|
|
|
|
|
|
|
|
bool isLoading = false; // 是否在加载中
|
|
|
|
|
bool hasMore = true; // 是否还有更多数据
|
2025-08-21 10:50:38 +08:00
|
|
|
|
final RxBool _throttleFlag = false.obs; // 滚动节流锁
|
2025-07-21 15:46:30 +08:00
|
|
|
|
|
|
|
|
|
// 表情json
|
|
|
|
|
List emoJson = emotionData;
|
|
|
|
|
|
|
|
|
|
// 底部操作栏模块
|
|
|
|
|
TextEditingController editorController = TextEditingController();
|
|
|
|
|
FocusNode editorFocusNode = FocusNode();
|
|
|
|
|
bool voiceBtnEnable = false; // 语音按钮
|
|
|
|
|
bool voicePanelEnable = false; // 语音操作面板
|
|
|
|
|
bool voiceToTransfer = false; // 语音转文字中
|
|
|
|
|
int voiceType = 0; // 语音操作类型
|
|
|
|
|
Map voiceTypeMap = {
|
|
|
|
|
0: '按住 说话', // 按住说话
|
|
|
|
|
1: '松开 发送', // 松开发送
|
|
|
|
|
2: '松开 取消', // 松开取消(左滑)
|
|
|
|
|
3: '语音转文字', // 语音转文字(右滑)
|
|
|
|
|
};
|
|
|
|
|
bool toolbarEnable = false; // 显示表情/选择区域
|
|
|
|
|
int toolbarIndex = 0; // 0 表情 1 选择
|
|
|
|
|
double keyboardHeight = 157.6; // 键盘高度
|
|
|
|
|
List chooseOptions = [
|
|
|
|
|
{'key': 'photo', 'name': '相册', 'icon': 'assets/images/icon_photo.webp'},
|
|
|
|
|
{'key': 'camera', 'name': '拍摄', 'icon': 'assets/images/icon_camera.webp'},
|
|
|
|
|
{'key': 'location', 'name': '位置', 'icon': 'assets/images/icon_location.webp'},
|
|
|
|
|
{'key': 'redpacket', 'name': '红包', 'icon': 'assets/images/icon_hb.webp'},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// controller监听
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// ScrollController chatController = ScrollController();
|
|
|
|
|
late ScrollController chatController;
|
|
|
|
|
|
2025-07-21 15:46:30 +08:00
|
|
|
|
ScrollController emojController = ScrollController();
|
|
|
|
|
|
|
|
|
|
// 模拟开红包按钮动画
|
|
|
|
|
late AnimationController animController;
|
|
|
|
|
|
|
|
|
|
// 创建一个从 0 到 π 的旋转动画
|
|
|
|
|
late Animation<double> animTurns;
|
|
|
|
|
|
|
|
|
|
// 初始化状态
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
2025-08-21 10:50:38 +08:00
|
|
|
|
final arg = Get.arguments as V2TimConversation;
|
|
|
|
|
arguments = arg.obs;
|
|
|
|
|
controller = Get.find<ChatDetailController>();
|
|
|
|
|
chatController = controller.chatController;
|
2025-07-21 15:46:30 +08:00
|
|
|
|
|
|
|
|
|
animController = AnimationController(
|
|
|
|
|
vsync: this,
|
|
|
|
|
duration: Duration(milliseconds: 500),
|
|
|
|
|
);
|
|
|
|
|
animTurns = Tween<double>(begin: 0, end: 3.1415926).animate(animController);
|
|
|
|
|
|
|
|
|
|
cleanUnRead();
|
|
|
|
|
|
|
|
|
|
getUserId();
|
|
|
|
|
|
|
|
|
|
getMsgData(initFlag: true);
|
|
|
|
|
|
|
|
|
|
// 编辑框获取焦点
|
|
|
|
|
editorFocusNode.addListener(() {
|
|
|
|
|
if (editorFocusNode.hasFocus) {
|
|
|
|
|
setState(() {
|
|
|
|
|
toolbarEnable = false;
|
|
|
|
|
});
|
2025-08-21 10:50:38 +08:00
|
|
|
|
controller.scrollToBottom();
|
2025-07-21 15:46:30 +08:00
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
// 滚动监听
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// Future.delayed(Duration(milliseconds: 1000), () {
|
|
|
|
|
|
|
|
|
|
// });
|
2025-07-21 15:46:30 +08:00
|
|
|
|
chatController.addListener(() {
|
2025-08-21 10:50:38 +08:00
|
|
|
|
if (_throttleFlag.value) return;
|
2025-07-21 15:46:30 +08:00
|
|
|
|
if (chatController.position.pixels >= chatController.position.maxScrollExtent - 50) {
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// if (chatController.position.pixels <= 50) {
|
|
|
|
|
_throttleFlag.value = true;
|
2025-07-21 15:46:30 +08:00
|
|
|
|
getMsgData().then((_) {
|
|
|
|
|
// 解锁
|
2025-08-21 10:50:38 +08:00
|
|
|
|
Future.delayed(Duration(milliseconds: 1000), () {
|
|
|
|
|
_throttleFlag.value = false;
|
2025-07-21 15:46:30 +08:00
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
if (Get.isRegistered<ChatDetailController>()) {
|
|
|
|
|
Get.delete<ChatDetailController>();
|
|
|
|
|
}
|
|
|
|
|
emojController.dispose();
|
|
|
|
|
editorFocusNode.dispose();
|
|
|
|
|
animController.dispose();
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// 设置好友备注
|
|
|
|
|
void setRemark() async {
|
|
|
|
|
String remark = '';
|
|
|
|
|
await MyDialog.confirm(
|
|
|
|
|
Column(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
TextField(
|
|
|
|
|
onChanged: (value) => remark = value,
|
|
|
|
|
maxLength: 16,
|
|
|
|
|
maxLengthEnforcement: MaxLengthEnforcement.enforced, // 强制不能输入超过
|
|
|
|
|
decoration: InputDecoration(
|
|
|
|
|
hintText: '请输入备注',
|
|
|
|
|
filled: true,
|
|
|
|
|
fillColor: const Color(0xFFF5F5F5),
|
|
|
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
|
|
|
|
enabledBorder: OutlineInputBorder(
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
borderSide: BorderSide.none,
|
|
|
|
|
),
|
|
|
|
|
focusedBorder: OutlineInputBorder(
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
borderSide: BorderSide(color: Color(0xFFBE4EFF), width: 1),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
title: '设置备注',
|
|
|
|
|
buttonText: '确认',
|
|
|
|
|
cancelText: '取消',
|
|
|
|
|
onConfirm: () async {
|
|
|
|
|
// print('备注为:$remark');
|
|
|
|
|
final res = await ImService.instance.setFriendInfo(userID: arguments.value.userID!, friendRemark: remark);
|
|
|
|
|
if (res.success) {
|
|
|
|
|
// 刷新会话列表数据
|
|
|
|
|
// Get.find<ChatController>().getConversationList();
|
|
|
|
|
arguments.update((val) {
|
|
|
|
|
val?.showName = remark;
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
print(res.desc);
|
|
|
|
|
print(arguments.value.userID);
|
|
|
|
|
MyDialog.toast(res.desc, icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-21 15:46:30 +08:00
|
|
|
|
void cleanUnRead() async {
|
2025-08-21 10:50:38 +08:00
|
|
|
|
if ((arguments.value.unreadCount ?? 0) > 0) {
|
|
|
|
|
final res = await ImService.instance.clearConversationUnreadCount(conversationID: arguments.value.conversationID);
|
2025-07-21 15:46:30 +08:00
|
|
|
|
if (!res.success) {
|
|
|
|
|
MyDialog.toast(res.desc, icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void getUserId() async {
|
|
|
|
|
final idRes = await ImService.instance.selfUserId();
|
|
|
|
|
if (idRes.success) {
|
|
|
|
|
selfUserId = idRes.data;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> getMsgData({bool initFlag = false}) async {
|
|
|
|
|
if (isLoading || !hasMore) return; // 正在加载 or 没有更多了
|
|
|
|
|
|
|
|
|
|
isLoading = true;
|
|
|
|
|
|
|
|
|
|
// 获取最旧一条消息作为游标
|
|
|
|
|
V2TimMessage? lastRealMsg;
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// for (var msg in controller.chatList.reversed) {
|
2025-07-21 15:46:30 +08:00
|
|
|
|
for (var msg in controller.chatList.reversed) {
|
|
|
|
|
if (msg.localCustomData != 'time_label') {
|
|
|
|
|
lastRealMsg = msg;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-21 10:50:38 +08:00
|
|
|
|
final lastMsg = lastRealMsg ?? arguments.value.lastMessage; // 如果找不到,就用传入的参数
|
|
|
|
|
print(lastMsg?.toLogString());
|
2025-07-21 15:46:30 +08:00
|
|
|
|
|
|
|
|
|
// final lastMsg = controller.chatList.isNotEmpty ? controller.chatList.last : arguments.lastMessage;
|
|
|
|
|
|
|
|
|
|
final res = await ImService.instance.getHistoryMessageList(
|
2025-08-21 10:50:38 +08:00
|
|
|
|
userID: arguments.value.userID,
|
2025-07-21 15:46:30 +08:00
|
|
|
|
lastMsg: lastMsg,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (res.success) {
|
|
|
|
|
final newMessages = res.data ?? [];
|
|
|
|
|
|
|
|
|
|
if (newMessages.isEmpty) {
|
|
|
|
|
hasMore = false;
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// MyDialog.toast('没有更多了~', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
|
|
|
|
|
}
|
|
|
|
|
if (initFlag && lastMsg != null) {
|
|
|
|
|
newMessages.insert(0, lastMsg);
|
|
|
|
|
// controller.scrollToBottom();
|
2025-07-21 15:46:30 +08:00
|
|
|
|
}
|
2025-08-21 10:50:38 +08:00
|
|
|
|
controller.updateChatListWithTimeLabels(newMessages);
|
|
|
|
|
if (initFlag) {
|
|
|
|
|
// 初始化时滚到最底部
|
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
|
|
if (chatController.hasClients) {
|
|
|
|
|
// controller.scrollToBottom();
|
|
|
|
|
// final bottomPadding = MediaQuery.of(context).padding.bottom; // 底部安全区域高度
|
|
|
|
|
// chatController.jumpTo(chatController.position.maxScrollExtent); // 60为底部操作栏高度
|
|
|
|
|
chatController.jumpTo(0);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
print('聊天数据加载成功');
|
2025-07-21 15:46:30 +08:00
|
|
|
|
} else {
|
|
|
|
|
MyDialog.toast("获取聊天记录失败:${res.desc}", icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isLoading = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 渲染聊天消息
|
|
|
|
|
List<Widget> renderChatList() {
|
|
|
|
|
List<Widget> msgtpl = [];
|
|
|
|
|
for (var item in controller.chatList) {
|
|
|
|
|
// 时间提示,公告提示
|
|
|
|
|
if (item.localCustomData == 'time_label') {
|
|
|
|
|
msgtpl.add(Container(
|
|
|
|
|
margin: const EdgeInsets.only(bottom: 15.0),
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
item.customElem!.data ?? '',
|
|
|
|
|
style: TextStyle(color: Colors.grey[600], fontSize: 12.0),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
));
|
|
|
|
|
}
|
2025-09-13 17:01:01 +08:00
|
|
|
|
// 写入记录的tips
|
|
|
|
|
else if (item.cloudCustomData == 'tips') {
|
|
|
|
|
msgtpl.add(
|
|
|
|
|
Container(
|
|
|
|
|
margin: const EdgeInsets.only(bottom: 15.0),
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
child: Text(
|
|
|
|
|
item.textElem!.text ?? '',
|
|
|
|
|
style: TextStyle(color: Colors.grey[600], fontSize: 12.0),
|
|
|
|
|
softWrap: true,
|
|
|
|
|
maxLines: 5,
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// 文本消息模板=1
|
2025-09-13 17:01:01 +08:00
|
|
|
|
else if (item.elemType == 1 && item.cloudCustomData != 'tips') {
|
2025-08-21 10:50:38 +08:00
|
|
|
|
msgtpl.add(
|
|
|
|
|
RenderChatItem(
|
|
|
|
|
data: item,
|
|
|
|
|
child: Ink(
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: !(item.isSelf ?? false) ? Color(0xFFFFFFFF) : Color(0xFF89E45B),
|
|
|
|
|
borderRadius: BorderRadius.circular(10.0),
|
|
|
|
|
),
|
|
|
|
|
child: InkWell(
|
|
|
|
|
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
|
|
|
|
borderRadius: BorderRadius.circular(10.0),
|
|
|
|
|
child: Container(
|
|
|
|
|
padding: const EdgeInsets.all(10.0),
|
|
|
|
|
child: RichTextUtil.getRichText(item.textElem?.text ?? '', color: !(item.isSelf ?? false) ? Colors.black : Colors.white), // 可自定义解析emoj/网址/电话
|
|
|
|
|
),
|
|
|
|
|
onLongPress: () {
|
|
|
|
|
contextMenuDialog();
|
|
|
|
|
},
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
);
|
2025-07-21 15:46:30 +08:00
|
|
|
|
}
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// gif表情模板=8
|
|
|
|
|
else if (item.elemType == 8) {
|
2025-07-21 15:46:30 +08:00
|
|
|
|
msgtpl.add(RenderChatItem(
|
|
|
|
|
data: item,
|
|
|
|
|
child: Ink(
|
|
|
|
|
child: InkWell(
|
|
|
|
|
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
|
|
|
|
child: Container(
|
|
|
|
|
constraints: const BoxConstraints(
|
|
|
|
|
maxHeight: 100.0,
|
|
|
|
|
maxWidth: 100.0,
|
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// child: Image.asset('assets/images/emotion/${item.faceElem?.data}'),
|
|
|
|
|
child: Image.asset('${item.faceElem?.data}'),
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
|
|
|
|
onLongPress: () {
|
|
|
|
|
contextMenuDialog();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
));
|
|
|
|
|
}
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// 图片模板=3
|
|
|
|
|
else if (item.elemType == 3) {
|
|
|
|
|
// List<String> imagePaths = item.imageElem?.imageList?.where((e) => e != null && e.url != null).map((e) => e!.url!).toList() ?? [];
|
|
|
|
|
final originImage = item.imageElem?.imageList?.firstWhere((e) => e?.type == 0 && e?.url != null, orElse: () => null);
|
|
|
|
|
List<String> imagePaths = originImage != null ? [originImage.url!] : [];
|
2025-07-21 15:46:30 +08:00
|
|
|
|
msgtpl.add(RenderChatItem(
|
|
|
|
|
data: item,
|
|
|
|
|
child: Ink(
|
|
|
|
|
child: InkWell(
|
2025-09-09 10:57:52 +08:00
|
|
|
|
onTap: () {
|
|
|
|
|
// 预览图片
|
|
|
|
|
Get.to(() => ImageViewer(
|
|
|
|
|
images: [imagePaths.first],
|
|
|
|
|
index: 0,
|
|
|
|
|
));
|
|
|
|
|
},
|
2025-07-21 15:46:30 +08:00
|
|
|
|
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
|
|
|
|
child: ClipRRect(
|
|
|
|
|
borderRadius: BorderRadius.circular(10.0),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// child: ImageGroup(
|
|
|
|
|
// images: imagePaths,
|
|
|
|
|
// width: 120,
|
|
|
|
|
// ),
|
|
|
|
|
child: Image.network(
|
|
|
|
|
imagePaths.first,
|
2025-07-21 15:46:30 +08:00
|
|
|
|
width: 120,
|
2025-08-21 10:50:38 +08:00
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
|
loadingBuilder: (context, child, loadingProgress) {
|
|
|
|
|
if (loadingProgress == null) {
|
|
|
|
|
// controller.scrollToBottom();
|
|
|
|
|
return child; // 加载完成,显示图片
|
|
|
|
|
}
|
|
|
|
|
return Container(
|
|
|
|
|
width: 120,
|
|
|
|
|
height: 240,
|
|
|
|
|
color: Colors.grey[300],
|
|
|
|
|
alignment: Alignment.center,
|
|
|
|
|
child: CircularProgressIndicator(
|
|
|
|
|
value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
errorBuilder: (context, error, stackTrace) {
|
|
|
|
|
return Container(
|
|
|
|
|
color: Colors.grey[300],
|
|
|
|
|
alignment: Alignment.center,
|
|
|
|
|
child: Icon(Icons.broken_image, color: Colors.grey, size: 40),
|
|
|
|
|
);
|
|
|
|
|
},
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
onLongPress: () {
|
|
|
|
|
contextMenuDialog();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
));
|
|
|
|
|
}
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// 视频模板=5
|
|
|
|
|
else if (item.elemType == 5) {
|
|
|
|
|
// print(item.videoElem!.toLogString());
|
2025-07-21 15:46:30 +08:00
|
|
|
|
msgtpl.add(RenderChatItem(
|
|
|
|
|
data: item,
|
|
|
|
|
child: Ink(
|
|
|
|
|
child: InkWell(
|
|
|
|
|
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
|
|
|
|
child: SizedBox(
|
2025-08-21 10:50:38 +08:00
|
|
|
|
width: 120.0,
|
2025-07-21 15:46:30 +08:00
|
|
|
|
child: Stack(
|
|
|
|
|
alignment: Alignment.center,
|
|
|
|
|
children: [
|
|
|
|
|
ClipRRect(
|
|
|
|
|
borderRadius: BorderRadius.circular(10.0),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
child: NetworkOrAssetImage(
|
|
|
|
|
imageUrl: item.videoElem?.snapshotUrl ?? '',
|
|
|
|
|
width: 120,
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const Align(
|
|
|
|
|
alignment: Alignment.center,
|
|
|
|
|
child: Icon(
|
|
|
|
|
Icons.play_circle,
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
size: 30.0,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
onTap: () {
|
2025-08-21 10:50:38 +08:00
|
|
|
|
showGeneralDialog(
|
|
|
|
|
context: context,
|
|
|
|
|
// barrierDismissible: true,
|
|
|
|
|
barrierColor: Colors.black.withAlpha((1.0 * 255).round()),
|
|
|
|
|
pageBuilder: (_, __, ___) {
|
|
|
|
|
return SafeArea(
|
|
|
|
|
child: PreviewVideo(
|
|
|
|
|
videoUrl: item.videoElem?.videoUrl ?? '',
|
|
|
|
|
width: item.videoElem?.snapshotWidth?.toDouble(),
|
|
|
|
|
height: item.videoElem?.snapshotHeight?.toDouble(),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
transitionBuilder: (_, anim, __, child) {
|
|
|
|
|
return FadeTransition(opacity: anim, child: child);
|
|
|
|
|
},
|
|
|
|
|
transitionDuration: const Duration(milliseconds: 200),
|
|
|
|
|
);
|
2025-07-21 15:46:30 +08:00
|
|
|
|
},
|
|
|
|
|
onLongPress: () {
|
|
|
|
|
contextMenuDialog();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
));
|
|
|
|
|
}
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// 语音模板=4
|
|
|
|
|
else if (item.elemType == 4) {
|
|
|
|
|
final durationMs = item.soundElem?.duration ?? 0;
|
|
|
|
|
final durationSeconds = (durationMs / 1000).round();
|
|
|
|
|
final maxWidth = (durationSeconds / 60 * 230).clamp(80.0, 230.0);
|
|
|
|
|
|
2025-07-21 15:46:30 +08:00
|
|
|
|
List<Widget> audiobody = [
|
|
|
|
|
Ink(
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: !(item.isSelf ?? false) ? const Color(0xFFFFFFFF) : const Color(0xFF89E45B),
|
|
|
|
|
borderRadius: BorderRadius.circular(10.0),
|
|
|
|
|
),
|
|
|
|
|
child: InkWell(
|
|
|
|
|
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
|
|
|
|
borderRadius: BorderRadius.circular(10.0),
|
|
|
|
|
child: Container(
|
|
|
|
|
padding: const EdgeInsets.all(10.0),
|
|
|
|
|
constraints: BoxConstraints(
|
2025-08-21 10:50:38 +08:00
|
|
|
|
maxWidth: maxWidth,
|
|
|
|
|
// maxWidth: (item.soundElem!.duration! / 1000) / 60 * 230,
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisAlignment: !(item.isSelf ?? false) ? MainAxisAlignment.start : MainAxisAlignment.end,
|
|
|
|
|
children: !(item.isSelf ?? false)
|
|
|
|
|
? [
|
|
|
|
|
const Icon(Icons.multitrack_audio),
|
|
|
|
|
const SizedBox(
|
|
|
|
|
width: 5.0,
|
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
Text('$durationSeconds"'),
|
2025-07-21 15:46:30 +08:00
|
|
|
|
]
|
|
|
|
|
: [
|
2025-08-21 10:50:38 +08:00
|
|
|
|
Text('$durationSeconds"'),
|
2025-07-21 15:46:30 +08:00
|
|
|
|
const SizedBox(
|
|
|
|
|
width: 5.0,
|
|
|
|
|
),
|
|
|
|
|
const Icon(Icons.multitrack_audio),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
onTap: () {
|
2025-08-21 10:50:38 +08:00
|
|
|
|
final locUrl = item.soundElem?.path ?? '';
|
|
|
|
|
final netUrl = item.soundElem?.url ?? '';
|
|
|
|
|
if (locUrl.isNotEmpty) {
|
|
|
|
|
AudioPlayerService().playNetwork(locUrl);
|
|
|
|
|
} else if (netUrl.isNotEmpty) {
|
|
|
|
|
AudioPlayerService().playLocal(netUrl);
|
|
|
|
|
} else {
|
|
|
|
|
MyDialog.toast('音频文件已过期', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
|
|
|
|
|
}
|
2025-07-21 15:46:30 +08:00
|
|
|
|
},
|
|
|
|
|
onLongPress: () {
|
|
|
|
|
contextMenuDialog();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(
|
|
|
|
|
width: 5.0,
|
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
|
|
|
|
|
// FStyle.badge(0, isdot: true),
|
2025-07-21 15:46:30 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if (item.isSelf ?? false) {
|
|
|
|
|
// 内容反转
|
|
|
|
|
audiobody = audiobody.reversed.toList();
|
|
|
|
|
} else {
|
|
|
|
|
audiobody = audiobody;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
msgtpl.add(RenderChatItem(
|
|
|
|
|
data: item,
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisAlignment: !(item.isSelf ?? false) ? MainAxisAlignment.start : MainAxisAlignment.end,
|
|
|
|
|
children: audiobody,
|
|
|
|
|
)));
|
|
|
|
|
}
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// 分享团购商品
|
|
|
|
|
else if (item.elemType == 2 && item.cloudCustomData == SummaryType.shareTuangou) {
|
2025-09-09 10:57:52 +08:00
|
|
|
|
// final makeJson = jsonEncode({
|
|
|
|
|
// "price": shopObj['price'],
|
|
|
|
|
// "title": shopObj['name'],
|
|
|
|
|
// "url": shopObj['pic'],
|
|
|
|
|
// "sell": Utils.graceNumber(int.parse(shopObj['sales'] ?? '0')),
|
|
|
|
|
// "goodsId": shopObj['id'],
|
|
|
|
|
// "userID": Get.find<ImUserInfoController>().userID.value,
|
|
|
|
|
// });
|
2025-08-21 10:50:38 +08:00
|
|
|
|
final obj = jsonDecode(item.customElem!.data!);
|
2025-09-04 22:19:56 +08:00
|
|
|
|
logger.e(obj);
|
2025-09-09 10:57:52 +08:00
|
|
|
|
final goodsId = obj['goodsId'];
|
|
|
|
|
final userID = obj['userID'];
|
2025-08-21 10:50:38 +08:00
|
|
|
|
final url = obj['url'];
|
|
|
|
|
final title = obj['title'];
|
|
|
|
|
final price = obj['price'];
|
2025-09-03 11:25:31 +08:00
|
|
|
|
final sell = Utils.graceNumber(int.tryParse(obj['sell'])!);
|
2025-08-21 10:50:38 +08:00
|
|
|
|
msgtpl.add(RenderChatItem(
|
|
|
|
|
data: item,
|
|
|
|
|
child: GestureDetector(
|
2025-09-09 10:57:52 +08:00
|
|
|
|
onTap: () {
|
|
|
|
|
// 这里带上分享人的ID
|
|
|
|
|
Get.toNamed('/goods', arguments: {'goodsId': goodsId, 'userID': userID});
|
|
|
|
|
},
|
2025-08-21 10:50:38 +08:00
|
|
|
|
child: Container(
|
|
|
|
|
width: 160,
|
|
|
|
|
clipBehavior: Clip.antiAlias,
|
|
|
|
|
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(15.0), boxShadow: [
|
|
|
|
|
BoxShadow(
|
|
|
|
|
color: Colors.black.withAlpha(5),
|
|
|
|
|
offset: Offset(0.0, 1.0),
|
|
|
|
|
blurRadius: 1.0,
|
|
|
|
|
spreadRadius: 0.0,
|
|
|
|
|
),
|
|
|
|
|
]),
|
|
|
|
|
child: Column(
|
|
|
|
|
children: [
|
|
|
|
|
NetworkOrAssetImage(
|
|
|
|
|
imageUrl: url,
|
|
|
|
|
width: 160.0,
|
|
|
|
|
),
|
|
|
|
|
Container(
|
|
|
|
|
padding: EdgeInsets.symmetric(horizontal: 10.0, vertical: 5.0),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
spacing: 5.0,
|
|
|
|
|
children: [
|
|
|
|
|
Text(
|
|
|
|
|
'$title',
|
|
|
|
|
style: TextStyle(fontSize: 14.0, height: 1.2),
|
|
|
|
|
maxLines: 2,
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
),
|
|
|
|
|
Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
|
|
|
children: [
|
|
|
|
|
Flexible(
|
|
|
|
|
child: Text.rich(
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
maxLines: 1,
|
|
|
|
|
TextSpan(style: TextStyle(color: Colors.red, fontSize: 12.0, fontWeight: FontWeight.w700, fontFamily: 'Arial'), children: [
|
|
|
|
|
TextSpan(text: '¥'),
|
|
|
|
|
TextSpan(
|
|
|
|
|
text: '$price',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 14.0,
|
|
|
|
|
)),
|
|
|
|
|
]),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
SizedBox(
|
|
|
|
|
width: 5,
|
|
|
|
|
),
|
|
|
|
|
Text(
|
|
|
|
|
'已售$sell件',
|
|
|
|
|
style: TextStyle(color: Colors.grey, fontSize: 10.0),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
// 分享短视频
|
|
|
|
|
else if (item.elemType == 2 && item.cloudCustomData == SummaryType.shareVideo) {
|
|
|
|
|
/// {imgUrl,videoUrl,width,height}
|
|
|
|
|
final obj = jsonDecode(item.customElem!.data!);
|
2025-09-04 22:19:56 +08:00
|
|
|
|
logger.e(obj);
|
2025-09-09 10:57:52 +08:00
|
|
|
|
final videoId = obj['videoId'];
|
2025-08-21 10:50:38 +08:00
|
|
|
|
final videoUrl = obj['videoUrl'];
|
|
|
|
|
final imgUrl = obj['imgUrl'];
|
|
|
|
|
final width = obj['width'] as num;
|
|
|
|
|
final height = obj['height'] as num;
|
2025-09-09 10:57:52 +08:00
|
|
|
|
final isHorizontal = width > height;
|
2025-08-21 10:50:38 +08:00
|
|
|
|
msgtpl.add(RenderChatItem(
|
|
|
|
|
data: item,
|
|
|
|
|
child: Ink(
|
|
|
|
|
child: InkWell(
|
|
|
|
|
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
|
|
|
|
child: SizedBox(
|
|
|
|
|
width: 120.0,
|
|
|
|
|
child: Stack(
|
|
|
|
|
alignment: Alignment.center,
|
|
|
|
|
children: [
|
|
|
|
|
ClipRRect(
|
|
|
|
|
borderRadius: BorderRadius.circular(10.0),
|
2025-09-09 10:57:52 +08:00
|
|
|
|
child: Container(
|
2025-08-21 10:50:38 +08:00
|
|
|
|
width: 120,
|
2025-09-09 10:57:52 +08:00
|
|
|
|
height: 240,
|
|
|
|
|
color: Colors.black,
|
|
|
|
|
child: NetworkOrAssetImage(
|
|
|
|
|
imageUrl: imgUrl,
|
|
|
|
|
fit: isHorizontal ? BoxFit.contain : BoxFit.cover,
|
|
|
|
|
placeholderAsset: 'assets/images/bk.jpg',
|
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const Align(
|
|
|
|
|
alignment: Alignment.center,
|
|
|
|
|
child: Icon(
|
|
|
|
|
Icons.play_circle,
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
size: 30.0,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
onTap: () {
|
2025-09-09 10:57:52 +08:00
|
|
|
|
Get.toNamed('/videoDetail', arguments: {'videoId': videoId});
|
|
|
|
|
// showGeneralDialog(
|
|
|
|
|
// context: context,
|
|
|
|
|
// barrierColor: Colors.black.withAlpha((1.0 * 255).round()),
|
|
|
|
|
// pageBuilder: (_, __, ___) {
|
|
|
|
|
// return SafeArea(
|
|
|
|
|
// bottom: true,
|
|
|
|
|
// child: Padding(
|
|
|
|
|
// padding: const EdgeInsets.only(bottom: 4),
|
|
|
|
|
// child: PreviewVideo(
|
|
|
|
|
// videoUrl: videoUrl,
|
|
|
|
|
// width: width.toDouble(),
|
|
|
|
|
// height: height.toDouble(),
|
|
|
|
|
// ),
|
|
|
|
|
// ),
|
|
|
|
|
// );
|
|
|
|
|
// },
|
|
|
|
|
// transitionBuilder: (_, anim, __, child) {
|
|
|
|
|
// return FadeTransition(opacity: anim, child: child);
|
|
|
|
|
// },
|
|
|
|
|
// transitionDuration: const Duration(milliseconds: 200),
|
|
|
|
|
// );
|
2025-08-21 10:50:38 +08:00
|
|
|
|
},
|
2025-09-09 10:57:52 +08:00
|
|
|
|
// onLongPress: () {
|
|
|
|
|
// contextMenuDialog();
|
|
|
|
|
// },
|
2025-08-21 10:50:38 +08:00
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
// 红包模板=自定义=2;
|
|
|
|
|
else if (item.elemType == 2 && item.cloudCustomData == SummaryType.hongbao) {
|
|
|
|
|
final obj = jsonDecode(item.customElem!.data!);
|
|
|
|
|
final open = obj['open'] ?? false;
|
|
|
|
|
final remark = obj['remark'];
|
|
|
|
|
// final maxNum = obj['maxNum'];
|
2025-07-21 15:46:30 +08:00
|
|
|
|
msgtpl.add(RenderChatItem(
|
|
|
|
|
data: item,
|
|
|
|
|
child: Ink(
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: const Color(0xFFFF7F43),
|
|
|
|
|
borderRadius: BorderRadius.circular(10.0),
|
|
|
|
|
),
|
|
|
|
|
child: InkWell(
|
|
|
|
|
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
|
|
|
|
borderRadius: BorderRadius.circular(10.0),
|
|
|
|
|
child: Container(
|
|
|
|
|
constraints: const BoxConstraints(
|
|
|
|
|
maxWidth: 210.0,
|
|
|
|
|
),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Container(
|
2025-08-21 10:50:38 +08:00
|
|
|
|
width: 210.0,
|
2025-07-21 15:46:30 +08:00
|
|
|
|
padding: const EdgeInsets.all(10.0),
|
|
|
|
|
child: Row(
|
|
|
|
|
children: <Widget>[
|
2025-08-21 10:50:38 +08:00
|
|
|
|
open
|
|
|
|
|
? Icon(Icons.check_circle, size: 32.0, color: Colors.white70)
|
|
|
|
|
: Image.asset(
|
|
|
|
|
'assets/images/hbico.png',
|
|
|
|
|
width: 32.0,
|
|
|
|
|
fit: BoxFit.contain,
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 10),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Text(
|
|
|
|
|
'$remark',
|
|
|
|
|
style: const TextStyle(color: Colors.white, fontSize: 14.0),
|
|
|
|
|
maxLines: 1,
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
),
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Container(
|
|
|
|
|
margin: const EdgeInsets.symmetric(horizontal: 10.0),
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 5.0),
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
decoration: const BoxDecoration(border: Border(top: BorderSide(color: Colors.white30, width: .5))),
|
|
|
|
|
child: const Text(
|
2025-08-21 10:50:38 +08:00
|
|
|
|
'红包',
|
2025-07-21 15:46:30 +08:00
|
|
|
|
style: TextStyle(color: Colors.white70, fontSize: 11.0),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
onTap: () {
|
|
|
|
|
receiveRedPacketDialog(item);
|
|
|
|
|
},
|
|
|
|
|
onLongPress: () {
|
|
|
|
|
contextMenuDialog();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
));
|
|
|
|
|
}
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// 位置模板=7
|
|
|
|
|
else if (item.elemType == 7) {
|
2025-07-21 15:46:30 +08:00
|
|
|
|
msgtpl.add(RenderChatItem(
|
|
|
|
|
data: item,
|
|
|
|
|
child: Ink(
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: const Color(0xFFFFFFFF),
|
|
|
|
|
borderRadius: BorderRadius.circular(10.0),
|
|
|
|
|
),
|
|
|
|
|
child: InkWell(
|
|
|
|
|
// splashColor: Colors.transparent,
|
|
|
|
|
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
|
|
|
|
borderRadius: BorderRadius.circular(10.0),
|
|
|
|
|
child: Container(
|
|
|
|
|
constraints: const BoxConstraints(
|
|
|
|
|
maxWidth: 210.0,
|
|
|
|
|
),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Container(
|
|
|
|
|
padding: const EdgeInsets.symmetric(
|
|
|
|
|
horizontal: 10.0,
|
|
|
|
|
vertical: 5.0,
|
|
|
|
|
),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: <Widget>[
|
|
|
|
|
Text(
|
|
|
|
|
item.locationElem?.desc ?? '位置信息异常',
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
),
|
|
|
|
|
Text(
|
|
|
|
|
"${item.locationElem?.latitude},${item.locationElem?.longitude}",
|
|
|
|
|
style: const TextStyle(color: Colors.grey, fontSize: 12.0),
|
|
|
|
|
overflow: TextOverflow.ellipsis,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
ClipRRect(
|
|
|
|
|
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(10.0)),
|
|
|
|
|
child: Image.asset('assets/images/map.jpg', width: 210.0, height: 70.0, fit: BoxFit.cover),
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
onTap: () {
|
|
|
|
|
MyDialog.toast('该功能暂未支持~', icon: Icon(Icons.warning), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
|
|
|
|
|
},
|
|
|
|
|
onLongPress: () {
|
|
|
|
|
contextMenuDialog();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return msgtpl;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 表情列表集合
|
|
|
|
|
List<Widget> renderEmojWidget() {
|
|
|
|
|
return [
|
|
|
|
|
// Tab切换
|
|
|
|
|
Container(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 5.0),
|
|
|
|
|
child: Row(
|
|
|
|
|
children: emoJson.map((item) {
|
|
|
|
|
return InkWell(
|
|
|
|
|
child: Container(
|
|
|
|
|
margin: const EdgeInsets.all(5.0),
|
|
|
|
|
alignment: Alignment.center,
|
|
|
|
|
height: 40.0,
|
|
|
|
|
width: 40.0,
|
|
|
|
|
decoration: BoxDecoration(color: item['selected'] ? Colors.white : Colors.transparent, borderRadius: BorderRadius.circular(5.0)),
|
|
|
|
|
child: item['index'] == 0
|
|
|
|
|
? Text(
|
|
|
|
|
item['pathLabel'],
|
|
|
|
|
style: const TextStyle(fontSize: 22.0),
|
|
|
|
|
)
|
|
|
|
|
: Image.asset(item['pathLabel'], height: 24.0, width: 24.0, fit: BoxFit.cover),
|
|
|
|
|
),
|
|
|
|
|
onTap: () {
|
|
|
|
|
handleEmojTab(item['index']);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}).toList(),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Container(
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.grey[200],
|
|
|
|
|
border: const Border(top: BorderSide(color: Colors.black54, width: .1)),
|
|
|
|
|
),
|
|
|
|
|
child: ListView(
|
|
|
|
|
controller: emojController,
|
|
|
|
|
padding: const EdgeInsets.all(10.0),
|
|
|
|
|
children: emoJson.map((item) {
|
|
|
|
|
return Visibility(
|
|
|
|
|
visible: item['selected'],
|
|
|
|
|
child: GridView(
|
|
|
|
|
shrinkWrap: true,
|
|
|
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
|
|
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
|
|
|
// 横轴元素个数
|
|
|
|
|
crossAxisCount: item['type'] == 'emoj' ? 8 : 5,
|
|
|
|
|
// 纵轴间距
|
|
|
|
|
mainAxisSpacing: 5.0,
|
|
|
|
|
// 横轴间距
|
|
|
|
|
crossAxisSpacing: 5.0,
|
|
|
|
|
// 子组件宽高比例
|
|
|
|
|
childAspectRatio: 1,
|
|
|
|
|
),
|
|
|
|
|
children: item['nodes'].map<Widget>((emoj) {
|
|
|
|
|
if (item['type'] == 'emoj') {
|
|
|
|
|
return Material(
|
|
|
|
|
type: MaterialType.transparency,
|
|
|
|
|
child: InkWell(
|
|
|
|
|
borderRadius: BorderRadius.circular(5.0),
|
|
|
|
|
child: Container(
|
|
|
|
|
alignment: Alignment.center,
|
|
|
|
|
height: 40.0,
|
|
|
|
|
width: 40.0,
|
|
|
|
|
child: Text(
|
|
|
|
|
emoj,
|
|
|
|
|
style: const TextStyle(fontSize: 24.0),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
onTap: () {
|
|
|
|
|
handleEmojClick(emoj);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
return Material(
|
|
|
|
|
type: MaterialType.transparency,
|
|
|
|
|
child: InkWell(
|
|
|
|
|
borderRadius: BorderRadius.circular(5.0),
|
|
|
|
|
child: Container(
|
|
|
|
|
alignment: Alignment.center,
|
|
|
|
|
padding: const EdgeInsets.all(5.0),
|
|
|
|
|
height: 68.0,
|
|
|
|
|
width: 68.0,
|
|
|
|
|
child: Image.asset(emoj),
|
|
|
|
|
),
|
|
|
|
|
onTap: () {
|
2025-08-21 10:50:38 +08:00
|
|
|
|
handleGIFClick(emoj, item['index']);
|
2025-07-21 15:46:30 +08:00
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}).toList(),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}).toList(),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 选择功能列表
|
|
|
|
|
List<Widget> renderChooseWidget() {
|
|
|
|
|
return [
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Container(
|
|
|
|
|
padding: const EdgeInsets.fromLTRB(30.0, 35.0, 30.0, 15.0),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.grey[200],
|
|
|
|
|
border: const Border(top: BorderSide(color: Colors.black38, width: .1)),
|
|
|
|
|
),
|
|
|
|
|
child: GridView(
|
|
|
|
|
shrinkWrap: true,
|
|
|
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
|
|
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
|
|
|
|
// 横轴元素个数
|
|
|
|
|
crossAxisCount: 4,
|
|
|
|
|
// 纵轴间距
|
|
|
|
|
mainAxisSpacing: 30.0,
|
|
|
|
|
// 横轴间距
|
|
|
|
|
crossAxisSpacing: 25.0,
|
|
|
|
|
// 子组件宽高比例
|
|
|
|
|
childAspectRatio: .8,
|
|
|
|
|
),
|
|
|
|
|
children: chooseOptions.map((item) {
|
|
|
|
|
return Column(
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Material(
|
|
|
|
|
type: MaterialType.transparency,
|
|
|
|
|
child: Ink(
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
borderRadius: BorderRadius.circular(15.0),
|
|
|
|
|
),
|
|
|
|
|
child: InkWell(
|
|
|
|
|
borderRadius: BorderRadius.circular(15.0),
|
|
|
|
|
child: Image.asset(item['icon'], height: 40.0, fit: BoxFit.cover),
|
|
|
|
|
onTap: () {
|
|
|
|
|
handleChooseAction(item['key']);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 5.0),
|
|
|
|
|
Text(
|
|
|
|
|
item['name'],
|
|
|
|
|
style: const TextStyle(color: Colors.black87, fontSize: 12.0),
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}).toList(),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------- { 聊天消息模块 } ---------- */
|
|
|
|
|
// 聊天消息滚动到底部
|
|
|
|
|
// void scrollToBottom() async {
|
|
|
|
|
// chatList = await fetchChatList();
|
|
|
|
|
// chatController.animateTo(isNeedScrollBottom ? 0 : chatController.position.maxScrollExtent,
|
|
|
|
|
// duration: const Duration(milliseconds: 200), curve: Curves.easeIn);
|
|
|
|
|
// }
|
2025-08-21 10:50:38 +08:00
|
|
|
|
|
|
|
|
|
// void scrollToBottom() {
|
|
|
|
|
// Future.delayed(Duration(milliseconds: 300), () {
|
|
|
|
|
// if (chatController.hasClients) {
|
|
|
|
|
// chatController.animateTo(
|
|
|
|
|
// // 0, // reverse: true 时滚动到底部是 offset: 0
|
|
|
|
|
// chatController.position.maxScrollExtent,
|
|
|
|
|
// duration: Duration(milliseconds: 300),
|
|
|
|
|
// curve: Curves.easeOut,
|
|
|
|
|
// );
|
|
|
|
|
// }
|
|
|
|
|
// });
|
|
|
|
|
// }
|
2025-07-21 15:46:30 +08:00
|
|
|
|
|
|
|
|
|
// 点击消息区域
|
|
|
|
|
void handleClickChatArea() {
|
|
|
|
|
hideKeyboard();
|
|
|
|
|
setState(() {
|
|
|
|
|
toolbarEnable = false;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------- { 底部Toolbar模块 } ---------- */
|
|
|
|
|
// 光标处插入内容
|
|
|
|
|
void insertTextAtCursor(String html) {
|
|
|
|
|
var editorNotifier = editorController.value; // The current value stored in this notifier.
|
|
|
|
|
var start = editorNotifier.selection.baseOffset;
|
|
|
|
|
var end = editorNotifier.selection.extentOffset;
|
|
|
|
|
if (editorNotifier.selection.isValid) {
|
|
|
|
|
String newText = '';
|
|
|
|
|
if (editorNotifier.selection.isCollapsed) {
|
|
|
|
|
if (end > 0) {
|
|
|
|
|
newText += editorNotifier.text.substring(0, end);
|
|
|
|
|
}
|
|
|
|
|
newText += html;
|
|
|
|
|
if (editorNotifier.text.length > end) {
|
|
|
|
|
newText += editorNotifier.text.substring(end, editorNotifier.text.length);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
newText = editorNotifier.text.replaceRange(start, end, html);
|
|
|
|
|
end = start;
|
|
|
|
|
}
|
|
|
|
|
editorController.value =
|
|
|
|
|
editorNotifier.copyWith(text: newText, selection: editorNotifier.selection.copyWith(baseOffset: end + html.length, extentOffset: end + html.length));
|
|
|
|
|
} else {
|
|
|
|
|
editorController.value = TextEditingValue(
|
|
|
|
|
text: html,
|
|
|
|
|
selection: TextSelection.fromPosition(TextPosition(offset: html.length)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 发送消息队列
|
2025-09-09 10:57:52 +08:00
|
|
|
|
Future<void> sendMessage(message) async {
|
2025-07-21 15:46:30 +08:00
|
|
|
|
// 待插入的消息
|
|
|
|
|
List<V2TimMessage> messagesToInsert = [];
|
|
|
|
|
V2TimMessage? lastRealMsg;
|
|
|
|
|
|
|
|
|
|
for (var msg in controller.chatList) {
|
|
|
|
|
if (msg.localCustomData != 'time_label') {
|
|
|
|
|
lastRealMsg = msg;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果有数据,检测时间,是否需要插入伪消息
|
|
|
|
|
if (lastRealMsg != null &&
|
|
|
|
|
needInsertTimeLabel(
|
|
|
|
|
(lastRealMsg.timestamp ?? 0) * 1000, // 转为毫秒级
|
|
|
|
|
DateTime.now().millisecondsSinceEpoch,
|
|
|
|
|
)) {
|
|
|
|
|
// 消息时间间隔超过3分钟插入伪消息
|
2025-09-03 11:25:31 +08:00
|
|
|
|
final showLabel = Utils.formatChatTime(DateTime.now().millisecondsSinceEpoch ~/ 1000);
|
2025-07-21 15:46:30 +08:00
|
|
|
|
final resMsg = await IMMessage().insertTimeLabel(showLabel, selfUserId);
|
|
|
|
|
messagesToInsert.add(resMsg.data);
|
|
|
|
|
} else {
|
|
|
|
|
// 没数据的时候直接插入伪消息
|
2025-09-03 11:25:31 +08:00
|
|
|
|
final showLabel = Utils.formatChatTime(DateTime.now().millisecondsSinceEpoch ~/ 1000);
|
2025-07-21 15:46:30 +08:00
|
|
|
|
|
|
|
|
|
final resMsg = await IMMessage().insertTimeLabel(showLabel, selfUserId);
|
|
|
|
|
messagesToInsert.add(resMsg.data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 不需要时间标签
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// 消息类型
|
|
|
|
|
late final ImResult res;
|
|
|
|
|
res = await IMMessage().sendMessage(
|
|
|
|
|
msg: message,
|
|
|
|
|
toUserID: arguments.value.userID,
|
2025-07-21 15:46:30 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (res.success && res.data != null) {
|
|
|
|
|
messagesToInsert.insert(0, res.data); // 加入消息本体
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// messagesToInsert.add(res.data); // 加入消息本体
|
2025-07-21 15:46:30 +08:00
|
|
|
|
|
|
|
|
|
controller.chatList.insertAll(0, messagesToInsert);
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// controller.chatList.addAll(messagesToInsert);
|
2025-07-21 15:46:30 +08:00
|
|
|
|
|
2025-08-21 10:50:38 +08:00
|
|
|
|
controller.scrollToBottom();
|
2025-07-21 15:46:30 +08:00
|
|
|
|
print('发送成功');
|
|
|
|
|
} else {
|
|
|
|
|
print('消息发送失败: ${res.code} - ${res.desc}');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool needInsertTimeLabel(int lastTimestamp, int newTimestamp, {int interval = 3 * 60}) {
|
|
|
|
|
return (newTimestamp - lastTimestamp) > interval * 1000;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 隐藏键盘
|
|
|
|
|
void hideKeyboard() {
|
|
|
|
|
if (editorFocusNode.hasFocus) {
|
|
|
|
|
editorFocusNode.unfocus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 表情/选择切换
|
|
|
|
|
void handleEmojChooseState(index) {
|
|
|
|
|
hideKeyboard();
|
|
|
|
|
setState(() {
|
|
|
|
|
toolbarEnable = true;
|
|
|
|
|
toolbarIndex = index;
|
|
|
|
|
voiceBtnEnable = false;
|
|
|
|
|
});
|
2025-08-21 10:50:38 +08:00
|
|
|
|
controller.scrollToBottom();
|
2025-07-21 15:46:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 表情Tab切换
|
|
|
|
|
void handleEmojTab(index) {
|
|
|
|
|
var emols = emoJson;
|
|
|
|
|
for (var i = 0, len = emols.length; i < len; i++) {
|
|
|
|
|
emols[i]['selected'] = false;
|
|
|
|
|
}
|
|
|
|
|
emols[index]['selected'] = true;
|
|
|
|
|
setState(() {
|
|
|
|
|
emoJson = emols;
|
|
|
|
|
});
|
|
|
|
|
emojController.jumpTo(0);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// 点击表情插入到输入框
|
2025-07-21 15:46:30 +08:00
|
|
|
|
void handleEmojClick(emoj) {
|
|
|
|
|
insertTextAtCursor(emoj);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// 点击Gif大图发送=8
|
|
|
|
|
void handleGIFClick(gifpath, index) async {
|
2025-07-21 15:46:30 +08:00
|
|
|
|
// 消息队列
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// Map message = {
|
|
|
|
|
// 'contentType': 8,
|
|
|
|
|
// 'content': gifpath,
|
|
|
|
|
// };
|
|
|
|
|
final res = await IMMessage().createFaceMessage(data: gifpath, index: index);
|
|
|
|
|
if (res.success) {
|
|
|
|
|
sendMessage(res.data?.messageInfo);
|
|
|
|
|
}
|
2025-07-21 15:46:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// 发送文本消息=1
|
|
|
|
|
void handleSubmit() async {
|
2025-07-21 15:46:30 +08:00
|
|
|
|
if (editorController.text.isEmpty) return;
|
|
|
|
|
// 消息队列
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// Map message = {
|
|
|
|
|
// 'contentType': 1,
|
|
|
|
|
// 'content': editorController.text,
|
|
|
|
|
// };
|
|
|
|
|
final res = await IMMessage().createTextMessage(text: editorController.text);
|
|
|
|
|
if (res.success) {
|
|
|
|
|
sendMessage(res.data?.messageInfo);
|
|
|
|
|
editorController.clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 发红包消息
|
|
|
|
|
void sendHongbao(date) async {
|
|
|
|
|
final amount = date['amount']; //用户输入的金额
|
|
|
|
|
final remark = date['remark']; //用户输入的留言
|
|
|
|
|
final maxNum = date['maxNum']; //红包数量
|
|
|
|
|
|
|
|
|
|
// 先检测可用余额
|
|
|
|
|
final makeJson = jsonEncode({
|
|
|
|
|
"amount": amount,
|
|
|
|
|
"remark": remark,
|
|
|
|
|
"maxNum": maxNum,
|
|
|
|
|
"open": false,
|
|
|
|
|
});
|
|
|
|
|
final res = await IMMessage().createCustomMessage(data: makeJson);
|
|
|
|
|
if (res.success && (res.data != null)) {
|
|
|
|
|
final custMsg = res.data!.messageInfo;
|
|
|
|
|
custMsg!.cloudCustomData = SummaryType.hongbao;
|
|
|
|
|
sendMessage(res.data!.messageInfo);
|
|
|
|
|
Get.back();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 发送图片消息=3
|
|
|
|
|
void sendImage(imgPath) async {
|
|
|
|
|
final resImg = await IMMessage().createImageMessage(imagePath: imgPath);
|
|
|
|
|
if (resImg.success) {
|
|
|
|
|
sendMessage(resImg.data?.messageInfo);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 发送语音消息=4
|
|
|
|
|
void sendVoiceMsg() async {
|
|
|
|
|
final fileMap = await VoiceService().stopRecording();
|
|
|
|
|
if (fileMap != null) {
|
|
|
|
|
final res = await IMMessage().createSoundMessage(
|
|
|
|
|
soundPath: fileMap['path'],
|
|
|
|
|
duration: fileMap['duration'],
|
|
|
|
|
);
|
|
|
|
|
if (res.success && res.data != null) {
|
|
|
|
|
sendMessage(res.data!.messageInfo);
|
|
|
|
|
} else {
|
|
|
|
|
MyDialog.toast('创建语音消息失败');
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
MyDialog.toast('语音限制1-60秒');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 发送视频消息=5
|
|
|
|
|
void sendVideo(videoFilePath, type, duration, snapshotPath) async {
|
2025-09-09 10:57:52 +08:00
|
|
|
|
final instance = MyDialog.loading('处理中');
|
|
|
|
|
|
2025-08-21 10:50:38 +08:00
|
|
|
|
final resImg = await IMMessage().createVideoMessage(
|
|
|
|
|
videoFilePath: videoFilePath,
|
|
|
|
|
type: type,
|
|
|
|
|
duration: duration,
|
|
|
|
|
snapshotPath: snapshotPath,
|
|
|
|
|
);
|
|
|
|
|
if (resImg.success) {
|
2025-09-09 10:57:52 +08:00
|
|
|
|
await sendMessage(resImg.data?.messageInfo);
|
|
|
|
|
instance.close();
|
2025-08-21 10:50:38 +08:00
|
|
|
|
}
|
2025-07-21 15:46:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-09 10:57:52 +08:00
|
|
|
|
// 拍摄
|
|
|
|
|
Future<void> showPicker(BuildContext context) async {
|
|
|
|
|
showModalBottomSheet(
|
|
|
|
|
context: context,
|
|
|
|
|
backgroundColor: Colors.white, // 透明底色,这样圆角才能生效
|
|
|
|
|
|
|
|
|
|
builder: (context) {
|
|
|
|
|
return SafeArea(
|
|
|
|
|
child: Column(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: [
|
|
|
|
|
InkWell(
|
|
|
|
|
onTap: () async {
|
|
|
|
|
Navigator.pop(context);
|
|
|
|
|
await _pickPhoto();
|
|
|
|
|
},
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
children: const [
|
|
|
|
|
Icon(Icons.photo),
|
|
|
|
|
SizedBox(width: 8),
|
|
|
|
|
Text("拍摄照片"),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
InkWell(
|
|
|
|
|
onTap: () async {
|
|
|
|
|
Navigator.pop(context);
|
|
|
|
|
await _pickVideo();
|
|
|
|
|
},
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
children: const [
|
|
|
|
|
Icon(Icons.videocam),
|
|
|
|
|
SizedBox(width: 8),
|
|
|
|
|
Text("拍摄视频"),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const Divider(height: 1),
|
|
|
|
|
InkWell(
|
|
|
|
|
onTap: () => Navigator.pop(context),
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
children: const [
|
|
|
|
|
// Icon(Icons.close),
|
|
|
|
|
// SizedBox(width: 8),
|
|
|
|
|
Text("取消"),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 拍照片
|
|
|
|
|
Future<void> _pickPhoto() async {
|
|
|
|
|
final XFile? photo = await _picker.pickImage(
|
|
|
|
|
source: ImageSource.camera,
|
|
|
|
|
maxWidth: 1920,
|
|
|
|
|
maxHeight: 1080,
|
|
|
|
|
imageQuality: 90,
|
|
|
|
|
);
|
|
|
|
|
if (photo != null) {
|
|
|
|
|
logger.w("照片路径: ${photo.path}");
|
|
|
|
|
sendImage(photo.path);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 拍视频
|
|
|
|
|
Future<void> _pickVideo() async {
|
|
|
|
|
final XFile? video = await _picker.pickVideo(
|
|
|
|
|
source: ImageSource.camera,
|
|
|
|
|
maxDuration: const Duration(minutes: 1),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (video == null) return;
|
|
|
|
|
logger.w("视频路径: ${video.path}");
|
|
|
|
|
// 获取首帧图
|
|
|
|
|
final snapshot = await generateVideoThumbnail(video.path) ?? '';
|
|
|
|
|
// 获取视频时长
|
|
|
|
|
final controller = VideoPlayerController.file(File(video.path));
|
|
|
|
|
await controller.initialize();
|
|
|
|
|
final duration = controller.value.duration.inSeconds;
|
|
|
|
|
await controller.dispose();
|
|
|
|
|
// mimeType
|
|
|
|
|
final mimeType = lookupMimeType(video.path) ?? 'video/mp4';
|
|
|
|
|
sendVideo(video.path, mimeType, duration, snapshot);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// 底部操作蓝选择区操作
|
2025-07-21 15:46:30 +08:00
|
|
|
|
void handleChooseAction(key) {
|
2025-09-09 10:57:52 +08:00
|
|
|
|
// MyDialog.toast('$key');
|
2025-07-21 15:46:30 +08:00
|
|
|
|
switch (key) {
|
|
|
|
|
case 'photo':
|
|
|
|
|
// ....
|
2025-08-21 10:50:38 +08:00
|
|
|
|
pickFile(context);
|
2025-07-21 15:46:30 +08:00
|
|
|
|
break;
|
|
|
|
|
case 'camera':
|
|
|
|
|
// ....
|
2025-09-09 10:57:52 +08:00
|
|
|
|
showPicker(context);
|
2025-07-21 15:46:30 +08:00
|
|
|
|
break;
|
|
|
|
|
case 'redpacket':
|
|
|
|
|
sendRedPacketDialog();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-21 10:50:38 +08:00
|
|
|
|
///从相册选取图片/视频
|
|
|
|
|
void pickFile(BuildContext context) async {
|
|
|
|
|
final pickedAssets = await AssetPicker.pickAssets(
|
|
|
|
|
context,
|
|
|
|
|
pickerConfig: AssetPickerConfig(
|
|
|
|
|
textDelegate: const AssetPickerTextDelegate(),
|
|
|
|
|
pathNameBuilder: (AssetPathEntity album) {
|
|
|
|
|
return Utils.translateAlbumName(album);
|
|
|
|
|
},
|
|
|
|
|
maxAssets: 5,
|
|
|
|
|
requestType: RequestType.common,
|
|
|
|
|
filterOptions: FilterOptionGroup(
|
|
|
|
|
imageOption: const FilterOption(),
|
|
|
|
|
videoOption: const FilterOption(
|
|
|
|
|
durationConstraint: DurationConstraint(
|
|
|
|
|
max: Duration(seconds: 120),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (pickedAssets != null && pickedAssets.isNotEmpty) {
|
|
|
|
|
for (final asset in pickedAssets) {
|
|
|
|
|
switch (asset.type) {
|
|
|
|
|
case AssetType.image:
|
|
|
|
|
print("选中了图片:${asset.title}");
|
|
|
|
|
var file = await asset.file;
|
|
|
|
|
if (file != null) {
|
|
|
|
|
var fileSizeInBytes = await file.length();
|
|
|
|
|
var sizeInMB = fileSizeInBytes / (1024 * 1024);
|
|
|
|
|
if (sizeInMB > 28) {
|
|
|
|
|
MyDialog.toast('图片大小不能超过28MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
|
|
|
|
|
} else {
|
|
|
|
|
print("图片合法,大小:$sizeInMB MB");
|
|
|
|
|
// 执行发送逻辑
|
|
|
|
|
sendImage(file.path);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
case AssetType.video:
|
|
|
|
|
print("选中了视频:${asset.title}");
|
|
|
|
|
var file = await asset.file;
|
|
|
|
|
if (file != null) {
|
|
|
|
|
var fileSizeInBytes = await file.length();
|
|
|
|
|
var sizeInMB = fileSizeInBytes / (1024 * 1024);
|
|
|
|
|
if (sizeInMB > 28) {
|
|
|
|
|
MyDialog.toast('图片大小不能超过28MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
|
|
|
|
|
} else {
|
|
|
|
|
print("图片合法,大小:$sizeInMB MB");
|
|
|
|
|
// 执行发送逻辑
|
|
|
|
|
var snapshot = await generateVideoThumbnail(file.path);
|
|
|
|
|
String? mimeType = await asset.mimeTypeAsync;
|
|
|
|
|
String vdType = mimeType?.split('/').last ?? 'mp4';
|
|
|
|
|
print(vdType);
|
|
|
|
|
sendVideo(file.path, vdType, asset.duration, snapshot);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
print("不支持的类型:${asset.type}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// final asset = pickedAssets.first;
|
|
|
|
|
// final file = await asset.file; // 获取实际文件
|
|
|
|
|
// if (file != null) {
|
|
|
|
|
// final fileSizeInBytes = await file.length();
|
|
|
|
|
// final sizeInMB = fileSizeInBytes / (1024 * 1024);
|
|
|
|
|
// if (sizeInMB > 100) {
|
|
|
|
|
// MyDialog.toast('图片大小不能超过100MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
|
|
|
|
|
// } else {
|
|
|
|
|
// print("图片合法,大小:$sizeInMB MB");
|
|
|
|
|
// //走upload(file)上传图片拿到url地址
|
|
|
|
|
// // file;
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-21 15:46:30 +08:00
|
|
|
|
/* ---------- { 弹窗功能模块 } ---------- */
|
|
|
|
|
// 红包弹窗
|
2025-08-21 10:50:38 +08:00
|
|
|
|
void receiveRedPacketDialog(V2TimMessage data) {
|
2025-07-21 15:46:30 +08:00
|
|
|
|
showDialog(
|
|
|
|
|
context: context,
|
|
|
|
|
builder: (context) {
|
2025-08-21 10:50:38 +08:00
|
|
|
|
final obj = jsonDecode(data.customElem!.data!);
|
|
|
|
|
final amount = obj['amount'];
|
|
|
|
|
final remark = obj['remark'];
|
|
|
|
|
final open = obj['open'] ?? false;
|
|
|
|
|
|
2025-07-21 15:46:30 +08:00
|
|
|
|
return Material(
|
|
|
|
|
type: MaterialType.transparency,
|
|
|
|
|
child: Column(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
children: [
|
|
|
|
|
Container(
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
margin: const EdgeInsets.symmetric(horizontal: 50.0),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
padding: const EdgeInsets.symmetric(vertical: 50.0, horizontal: 20.0),
|
2025-07-21 15:46:30 +08:00
|
|
|
|
decoration: const BoxDecoration(
|
|
|
|
|
color: Color(0xFFFF7F43),
|
|
|
|
|
borderRadius: BorderRadius.all(Radius.circular(12.0)),
|
|
|
|
|
),
|
|
|
|
|
child: Column(
|
|
|
|
|
children: [
|
|
|
|
|
ClipRRect(
|
|
|
|
|
borderRadius: BorderRadius.circular(5.0),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
child: NetworkOrAssetImage(
|
|
|
|
|
imageUrl: data.senderProfile?.faceUrl,
|
|
|
|
|
),
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
|
|
|
|
const SizedBox(
|
|
|
|
|
height: 5.0,
|
|
|
|
|
),
|
|
|
|
|
Text(
|
2025-08-21 10:50:38 +08:00
|
|
|
|
'${data.senderProfile?.nickName}的红包',
|
2025-07-21 15:46:30 +08:00
|
|
|
|
style: const TextStyle(color: Color(0xFFFFF9C7), fontWeight: FontWeight.w600),
|
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
SizedBox(height: 10),
|
|
|
|
|
Text(
|
|
|
|
|
amount,
|
|
|
|
|
style: const TextStyle(color: Color(0xFFFFF9C7), fontWeight: FontWeight.w500, fontSize: 20.0),
|
|
|
|
|
),
|
|
|
|
|
SizedBox(height: 20.0),
|
2025-07-21 15:46:30 +08:00
|
|
|
|
Text(
|
2025-08-21 10:50:38 +08:00
|
|
|
|
remark,
|
2025-07-21 15:46:30 +08:00
|
|
|
|
style: const TextStyle(color: Color(0xFFFFF9C7), fontWeight: FontWeight.w500, fontSize: 20.0),
|
|
|
|
|
),
|
|
|
|
|
SizedBox(
|
|
|
|
|
height: 100.0,
|
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
if (open == false && data.isSelf == false)
|
|
|
|
|
AnimatedBuilder(
|
|
|
|
|
animation: animTurns,
|
|
|
|
|
builder: (context, child) {
|
|
|
|
|
return Transform(
|
|
|
|
|
transform: Matrix4.rotationY(animTurns.value),
|
|
|
|
|
alignment: Alignment.center,
|
|
|
|
|
child: FilledButton(
|
|
|
|
|
style: ButtonStyle(
|
|
|
|
|
backgroundColor: WidgetStateProperty.all(const Color(0xFFFFF9C7)),
|
|
|
|
|
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
|
|
|
|
minimumSize: WidgetStateProperty.all(const Size(80.0, 80.0)),
|
|
|
|
|
shape: WidgetStateProperty.all(const CircleBorder()),
|
|
|
|
|
elevation: WidgetStateProperty.all(3.0),
|
|
|
|
|
),
|
|
|
|
|
child: Text(
|
|
|
|
|
'开',
|
|
|
|
|
style: TextStyle(color: Color(0xFF3B3B3B), fontSize: 28.0),
|
|
|
|
|
),
|
|
|
|
|
onPressed: () async {
|
|
|
|
|
// 点击开红包,开始动画
|
|
|
|
|
animController.repeat();
|
|
|
|
|
// 执行抢红包结果查询,(群)展示抢红包人员信息,单不用管
|
|
|
|
|
// 执行消费红包动作
|
|
|
|
|
//--------
|
|
|
|
|
// 成功后修改消息体
|
|
|
|
|
obj['open'] = true; //成功标记为true
|
|
|
|
|
data.customElem!.data = jsonEncode(obj);
|
|
|
|
|
ImService.instance.modifyMessage(message: data);
|
|
|
|
|
// 模拟开红包逻辑,1 秒后停止动画
|
|
|
|
|
Future.delayed(Duration(seconds: 1), () {
|
|
|
|
|
animController.stop();
|
|
|
|
|
animController.reset();
|
|
|
|
|
Get.back();
|
|
|
|
|
});
|
|
|
|
|
},
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
),
|
2025-07-21 15:46:30 +08:00
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
GestureDetector(
|
|
|
|
|
child: Container(
|
|
|
|
|
margin: const EdgeInsets.only(top: 20.0),
|
|
|
|
|
height: 30.0,
|
|
|
|
|
width: 30.0,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
border: Border.all(color: Colors.white, width: 1.5),
|
|
|
|
|
borderRadius: BorderRadius.circular(50.0),
|
|
|
|
|
),
|
|
|
|
|
child: const Icon(
|
|
|
|
|
Icons.close_outlined,
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
size: 18.0,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
onTap: () {
|
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 长按消息菜单
|
|
|
|
|
void contextMenuDialog() {
|
2025-09-09 10:57:52 +08:00
|
|
|
|
// showDialog(
|
|
|
|
|
// context: context,
|
|
|
|
|
// builder: (context) {
|
|
|
|
|
// return SimpleDialog(
|
|
|
|
|
// backgroundColor: Colors.white,
|
|
|
|
|
// surfaceTintColor: Colors.white,
|
|
|
|
|
// contentPadding: const EdgeInsets.symmetric(vertical: 5.0),
|
|
|
|
|
// shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15.0)),
|
|
|
|
|
// children: [
|
|
|
|
|
// SimpleDialogOption(
|
|
|
|
|
// child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('复制')),
|
|
|
|
|
// onPressed: () {},
|
|
|
|
|
// ),
|
|
|
|
|
// SimpleDialogOption(
|
|
|
|
|
// child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('发送给朋友')),
|
|
|
|
|
// onPressed: () {},
|
|
|
|
|
// ),
|
|
|
|
|
// SimpleDialogOption(
|
|
|
|
|
// child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('收藏')),
|
|
|
|
|
// onPressed: () {},
|
|
|
|
|
// ),
|
|
|
|
|
// SimpleDialogOption(
|
|
|
|
|
// child: const Padding(padding: EdgeInsets.symmetric(vertical: 5.0), child: Text('删除')),
|
|
|
|
|
// onPressed: () {},
|
|
|
|
|
// ),
|
|
|
|
|
// ],
|
|
|
|
|
// );
|
|
|
|
|
// },
|
|
|
|
|
// );
|
2025-07-21 15:46:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 发群红包弹窗
|
|
|
|
|
void sendRedPacketDialog() {
|
|
|
|
|
showModalBottomSheet(
|
|
|
|
|
backgroundColor: Colors.grey[50],
|
|
|
|
|
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(15.0))),
|
|
|
|
|
showDragHandle: true,
|
|
|
|
|
clipBehavior: Clip.hardEdge,
|
|
|
|
|
isScrollControlled: true, // 屏幕最大高度
|
|
|
|
|
constraints: BoxConstraints(
|
|
|
|
|
maxHeight: MediaQuery.of(context).size.height - 180, // 自定义最大高度
|
|
|
|
|
),
|
|
|
|
|
context: context,
|
|
|
|
|
builder: (context) {
|
2025-08-21 10:50:38 +08:00
|
|
|
|
return RedPacket(
|
|
|
|
|
flag: false,
|
|
|
|
|
onSend: (date) {
|
|
|
|
|
sendHongbao(date);
|
|
|
|
|
});
|
2025-07-21 15:46:30 +08:00
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------- { 其它功能模块 } ---------- */
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Stack(
|
|
|
|
|
fit: StackFit.expand,
|
|
|
|
|
children: [
|
|
|
|
|
// 页面主体(聊天消息区/底部操作区)
|
|
|
|
|
Scaffold(
|
|
|
|
|
backgroundColor: Colors.grey[200],
|
2025-08-21 10:50:38 +08:00
|
|
|
|
resizeToAvoidBottomInset: true, // 启用键盘自动避让
|
2025-07-21 15:46:30 +08:00
|
|
|
|
appBar: AppBar(
|
|
|
|
|
centerTitle: true,
|
|
|
|
|
backgroundColor: Colors.transparent,
|
|
|
|
|
foregroundColor: Colors.white,
|
|
|
|
|
leading: IconButton(
|
|
|
|
|
icon: Icon(
|
|
|
|
|
Icons.arrow_back_ios_rounded,
|
|
|
|
|
size: 20.0,
|
|
|
|
|
),
|
|
|
|
|
onPressed: () {
|
|
|
|
|
Get.back();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
titleSpacing: 1.0,
|
2025-08-21 10:50:38 +08:00
|
|
|
|
title: Obx(() {
|
|
|
|
|
return Text(
|
|
|
|
|
// '${arguments['title']}',
|
|
|
|
|
'${arguments.value.showName}',
|
|
|
|
|
style: const TextStyle(fontSize: 18.0, fontFamily: 'Arial'),
|
|
|
|
|
);
|
|
|
|
|
}),
|
2025-07-21 15:46:30 +08:00
|
|
|
|
flexibleSpace: Container(
|
|
|
|
|
decoration: const BoxDecoration(
|
|
|
|
|
gradient: LinearGradient(
|
|
|
|
|
begin: Alignment.topLeft,
|
|
|
|
|
end: Alignment.bottomRight,
|
|
|
|
|
colors: [Color(0xFFBE4EFF), Color(0xFF1DFFC7)],
|
|
|
|
|
)),
|
|
|
|
|
),
|
|
|
|
|
actions: [
|
|
|
|
|
IconButton(
|
|
|
|
|
icon: const Icon(
|
|
|
|
|
Icons.more_horiz,
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
onPressed: () async {
|
|
|
|
|
final paddingTop = MediaQuery.of(Get.context!).padding.top;
|
|
|
|
|
|
|
|
|
|
final selected = await showMenu(
|
|
|
|
|
context: Get.context!,
|
|
|
|
|
position: RelativeRect.fromLTRB(
|
|
|
|
|
double.infinity,
|
|
|
|
|
kToolbarHeight + paddingTop - 12,
|
|
|
|
|
8,
|
|
|
|
|
double.infinity,
|
|
|
|
|
),
|
|
|
|
|
color: FStyle.primaryColor,
|
|
|
|
|
elevation: 8,
|
|
|
|
|
items: [
|
2025-09-17 15:32:18 +08:00
|
|
|
|
// PopupMenuItem<String>(
|
|
|
|
|
// value: 'remark',
|
|
|
|
|
// child: Row(
|
|
|
|
|
// children: [
|
|
|
|
|
// Icon(Icons.edit, color: Colors.white, size: 18),
|
|
|
|
|
// SizedBox(width: 8),
|
|
|
|
|
// Text(
|
|
|
|
|
// '设置备注',
|
|
|
|
|
// style: TextStyle(color: Colors.white),
|
|
|
|
|
// ),
|
|
|
|
|
// ],
|
|
|
|
|
// ),
|
|
|
|
|
// ),
|
|
|
|
|
|
|
|
|
|
// PopupMenuItem<String>(
|
|
|
|
|
// value: 'not',
|
|
|
|
|
// child: Row(
|
|
|
|
|
// children: [
|
|
|
|
|
// Icon(Icons.do_not_disturb_on, color: Colors.white, size: 18),
|
|
|
|
|
// SizedBox(width: 8),
|
|
|
|
|
// Text(
|
|
|
|
|
// '设为免打扰',
|
|
|
|
|
// style: TextStyle(color: Colors.white),
|
|
|
|
|
// ),
|
|
|
|
|
// ],
|
|
|
|
|
// ),
|
|
|
|
|
// ),
|
|
|
|
|
|
2025-08-21 10:50:38 +08:00
|
|
|
|
PopupMenuItem<String>(
|
|
|
|
|
value: 'report',
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
Icon(Icons.report, color: Colors.white, size: 18),
|
|
|
|
|
SizedBox(width: 8),
|
|
|
|
|
Text(
|
|
|
|
|
'举报',
|
|
|
|
|
style: TextStyle(color: Colors.white),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
PopupMenuItem<String>(
|
|
|
|
|
value: 'block',
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
Icon(Icons.block, color: Colors.white, size: 18),
|
|
|
|
|
SizedBox(width: 8),
|
|
|
|
|
Text(
|
|
|
|
|
'拉黑',
|
|
|
|
|
style: TextStyle(color: Colors.white),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
2025-09-17 15:32:18 +08:00
|
|
|
|
// PopupMenuItem<String>(
|
|
|
|
|
// value: 'foucs',
|
|
|
|
|
// child: Row(
|
|
|
|
|
// children: [
|
|
|
|
|
// Icon(Icons.person_remove_alt_1, color: Colors.white, size: 18),
|
|
|
|
|
// SizedBox(width: 8),
|
|
|
|
|
// Text(
|
|
|
|
|
// '取消关注',
|
|
|
|
|
// style: TextStyle(color: Colors.white),
|
|
|
|
|
// ),
|
|
|
|
|
// ],
|
|
|
|
|
// ),
|
|
|
|
|
// ),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (selected != null) {
|
|
|
|
|
switch (selected) {
|
2025-09-17 15:32:18 +08:00
|
|
|
|
// case 'remark':
|
|
|
|
|
// print('点击了备注');
|
|
|
|
|
// setRemark();
|
|
|
|
|
// break;
|
|
|
|
|
|
|
|
|
|
// case 'not':
|
|
|
|
|
// print('点击了免打扰');
|
|
|
|
|
// break;
|
|
|
|
|
|
2025-08-21 10:50:38 +08:00
|
|
|
|
case 'report':
|
|
|
|
|
print('点击了举报');
|
|
|
|
|
break;
|
|
|
|
|
case 'block':
|
|
|
|
|
print('点击了拉黑');
|
|
|
|
|
break;
|
2025-09-17 15:32:18 +08:00
|
|
|
|
// case 'foucs':
|
|
|
|
|
// print('点击了取关');
|
|
|
|
|
// break;
|
2025-08-21 10:50:38 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
body: Flex(
|
|
|
|
|
direction: Axis.vertical,
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
// 渲染聊天消息
|
|
|
|
|
Expanded(
|
2025-08-21 10:50:38 +08:00
|
|
|
|
child: GestureDetector(
|
|
|
|
|
behavior: HitTestBehavior.opaque,
|
|
|
|
|
onTap: handleClickChatArea,
|
|
|
|
|
child: LayoutBuilder(
|
|
|
|
|
builder: (context, constraints) {
|
|
|
|
|
return Obx(() {
|
|
|
|
|
final msgWidgets = renderChatList().reversed.toList();
|
|
|
|
|
|
|
|
|
|
return ListView(
|
|
|
|
|
controller: chatController,
|
|
|
|
|
reverse: true,
|
|
|
|
|
padding: const EdgeInsets.all(10.0),
|
|
|
|
|
children: [
|
|
|
|
|
ConstrainedBox(
|
|
|
|
|
constraints: BoxConstraints(
|
|
|
|
|
minHeight: constraints.maxHeight - 40,
|
|
|
|
|
),
|
|
|
|
|
child: Column(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
children: msgWidgets,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
});
|
2025-07-21 15:46:30 +08:00
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
// 底部操作栏
|
|
|
|
|
Container(
|
2025-08-21 10:50:38 +08:00
|
|
|
|
color: Colors.grey[100],
|
|
|
|
|
child: SafeArea(
|
|
|
|
|
bottom: true,
|
|
|
|
|
child: Container(
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.grey[100],
|
|
|
|
|
border: const Border(top: BorderSide(color: Colors.black38, width: .1)),
|
|
|
|
|
),
|
|
|
|
|
child: Column(
|
|
|
|
|
children: [
|
|
|
|
|
// 输入框编辑器模块
|
|
|
|
|
Container(
|
|
|
|
|
padding: const EdgeInsets.all(10.0),
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
InkWell(
|
|
|
|
|
child: Icon(
|
|
|
|
|
voiceBtnEnable ? Icons.keyboard_outlined : Icons.contactless_outlined,
|
|
|
|
|
color: const Color(0xFF3B3B3B),
|
|
|
|
|
size: 30.0,
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
onTap: () {
|
|
|
|
|
setState(() {
|
|
|
|
|
toolbarEnable = false;
|
|
|
|
|
if (voiceBtnEnable) {
|
|
|
|
|
voiceBtnEnable = false;
|
|
|
|
|
editorFocusNode.requestFocus();
|
|
|
|
|
} else {
|
|
|
|
|
voiceBtnEnable = true;
|
|
|
|
|
editorFocusNode.unfocus();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(
|
|
|
|
|
width: 10.0,
|
|
|
|
|
),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Container(
|
|
|
|
|
constraints: const BoxConstraints(minHeight: 40.0),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
borderRadius: BorderRadius.circular(5),
|
|
|
|
|
),
|
|
|
|
|
child: Stack(
|
|
|
|
|
children: [
|
|
|
|
|
// 输入框
|
|
|
|
|
Offstage(
|
|
|
|
|
offstage: voiceBtnEnable,
|
|
|
|
|
child: TextField(
|
|
|
|
|
decoration: const InputDecoration(
|
|
|
|
|
isDense: true,
|
|
|
|
|
hoverColor: Colors.transparent,
|
|
|
|
|
contentPadding: EdgeInsets.all(8.0),
|
|
|
|
|
border: OutlineInputBorder(borderSide: BorderSide.none),
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
style: const TextStyle(
|
|
|
|
|
fontSize: 16.0,
|
|
|
|
|
),
|
|
|
|
|
maxLines: null,
|
|
|
|
|
controller: editorController,
|
|
|
|
|
focusNode: editorFocusNode,
|
|
|
|
|
cursorColor: const Color(0xFF07C160),
|
|
|
|
|
onChanged: (value) {},
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
),
|
|
|
|
|
// 语音
|
|
|
|
|
Offstage(
|
|
|
|
|
offstage: !voiceBtnEnable,
|
|
|
|
|
child: GestureDetector(
|
|
|
|
|
child: Container(
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
borderRadius: BorderRadius.circular(5),
|
|
|
|
|
),
|
|
|
|
|
alignment: Alignment.center,
|
|
|
|
|
height: 40.0,
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
child: Text(
|
|
|
|
|
voiceTypeMap[voiceType],
|
|
|
|
|
style: const TextStyle(fontSize: 15.0),
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
),
|
|
|
|
|
onPanStart: (details) async {
|
|
|
|
|
// 开始录音
|
|
|
|
|
final res = await VoiceService().startRecording();
|
|
|
|
|
if (res) {
|
2025-07-21 15:46:30 +08:00
|
|
|
|
setState(() {
|
|
|
|
|
voiceType = 1;
|
|
|
|
|
voicePanelEnable = true;
|
|
|
|
|
});
|
2025-08-21 10:50:38 +08:00
|
|
|
|
} else {
|
|
|
|
|
MyDialog.toast('未获得麦克风权限');
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onPanUpdate: (details) {
|
|
|
|
|
Offset pos = details.globalPosition;
|
|
|
|
|
double swipeY = MediaQuery.of(context).size.height - 120;
|
|
|
|
|
double swipeX = MediaQuery.of(context).size.width / 2 + 50;
|
|
|
|
|
setState(() {
|
|
|
|
|
if (pos.dy >= swipeY) {
|
|
|
|
|
voiceType = 1; // 松开发送
|
|
|
|
|
} else if (pos.dy < swipeY && pos.dx < swipeX) {
|
|
|
|
|
voiceType = 2; // 左滑松开取消
|
|
|
|
|
} else if (pos.dy < swipeY && pos.dx >= swipeX) {
|
|
|
|
|
voiceType = 3; // 右滑语音转文字
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
onPanEnd: (details) {
|
|
|
|
|
// print('停止录音');
|
|
|
|
|
setState(() {
|
|
|
|
|
switch (voiceType) {
|
|
|
|
|
case 1:
|
|
|
|
|
// MyDialog.toast('发送录音文件');
|
|
|
|
|
sendVoiceMsg();
|
|
|
|
|
voicePanelEnable = false;
|
|
|
|
|
break;
|
|
|
|
|
case 2:
|
|
|
|
|
// MyDialog.toast('取消发送');
|
|
|
|
|
VoiceService().cancelRecording;
|
|
|
|
|
voicePanelEnable = false;
|
|
|
|
|
break;
|
|
|
|
|
case 3:
|
|
|
|
|
MyDialog.toast('语音转文字');
|
|
|
|
|
voicePanelEnable = true;
|
|
|
|
|
voiceToTransfer = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
voiceType = 0;
|
|
|
|
|
});
|
|
|
|
|
},
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
),
|
|
|
|
|
],
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
),
|
|
|
|
|
const SizedBox(
|
|
|
|
|
width: 10.0,
|
|
|
|
|
),
|
|
|
|
|
InkWell(
|
|
|
|
|
child: const Icon(
|
|
|
|
|
Icons.add_reaction_rounded,
|
|
|
|
|
color: Color(0xFF3B3B3B),
|
|
|
|
|
size: 30.0,
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
onTap: () {
|
|
|
|
|
handleEmojChooseState(0);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(
|
|
|
|
|
width: 8.0,
|
|
|
|
|
),
|
|
|
|
|
InkWell(
|
|
|
|
|
child: const Icon(
|
|
|
|
|
Icons.add,
|
|
|
|
|
color: Color(0xFF3B3B3B),
|
|
|
|
|
size: 30.0,
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
onTap: () {
|
|
|
|
|
handleEmojChooseState(1);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(
|
|
|
|
|
width: 8.0,
|
|
|
|
|
),
|
|
|
|
|
InkWell(
|
|
|
|
|
child: Container(
|
|
|
|
|
height: 25.0,
|
|
|
|
|
width: 25.0,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: const Color(0xFF07C160),
|
|
|
|
|
borderRadius: BorderRadius.circular(20.0),
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
child: const Icon(
|
|
|
|
|
Icons.arrow_upward,
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
size: 20.0,
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
onTap: () {
|
|
|
|
|
handleSubmit();
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
),
|
2025-07-21 15:46:30 +08:00
|
|
|
|
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// 表情+选择模块
|
|
|
|
|
Visibility(
|
|
|
|
|
visible: toolbarEnable,
|
|
|
|
|
child: SizedBox(
|
|
|
|
|
height: keyboardHeight,
|
|
|
|
|
child: Column(
|
|
|
|
|
children: toolbarIndex == 0 ? renderEmojWidget() : renderChooseWidget(),
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
],
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
)
|
2025-07-21 15:46:30 +08:00
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
// 录音主体(按住说话/松开取消/语音转文本)
|
|
|
|
|
IgnorePointer(
|
|
|
|
|
ignoring: false,
|
|
|
|
|
child: Visibility(
|
|
|
|
|
visible: voicePanelEnable,
|
|
|
|
|
child: Material(
|
|
|
|
|
color: const Color(0xDD1B1B1B),
|
|
|
|
|
child: Stack(
|
|
|
|
|
children: [
|
|
|
|
|
// 取消发送+语音转文字
|
|
|
|
|
Positioned(
|
2025-08-21 10:50:38 +08:00
|
|
|
|
bottom: 160,
|
2025-07-21 15:46:30 +08:00
|
|
|
|
left: 30,
|
|
|
|
|
right: 30,
|
|
|
|
|
child: Visibility(
|
|
|
|
|
visible: !voiceToTransfer,
|
|
|
|
|
child: Column(
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// crossAxisAlignment: voiceType == 2 ? CrossAxisAlignment.start : CrossAxisAlignment.center,
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
2025-07-21 15:46:30 +08:00
|
|
|
|
children: [
|
|
|
|
|
// 语音动画层
|
|
|
|
|
Stack(
|
|
|
|
|
alignment: Alignment.bottomCenter,
|
|
|
|
|
children: [
|
|
|
|
|
AnimatedContainer(
|
|
|
|
|
duration: Duration(milliseconds: 200),
|
|
|
|
|
height: 70.0,
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// width: voiceType == 2 ? 70.0 : 200.0,
|
|
|
|
|
width: 200.0,
|
2025-07-21 15:46:30 +08:00
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: voiceType == 2 ? Colors.red : Color(0xFF89E45B),
|
|
|
|
|
borderRadius: BorderRadius.circular(15.0),
|
|
|
|
|
),
|
|
|
|
|
clipBehavior: Clip.antiAlias,
|
|
|
|
|
child: Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
children: [
|
|
|
|
|
Image.asset('assets/images/voice_waves.gif', height: 23.0, width: voiceType == 2 ? 30.0 : 70.0, fit: BoxFit.cover)
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
RotatedBox(
|
|
|
|
|
quarterTurns: 0,
|
|
|
|
|
child: CustomPaint(painter: ArrowShape(arrowColor: voiceType == 2 ? Colors.red : Color(0xFF89E45B), arrowSize: 10.0)),
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(
|
|
|
|
|
height: 50.0,
|
|
|
|
|
),
|
|
|
|
|
// 操作项
|
|
|
|
|
Row(
|
2025-08-21 10:50:38 +08:00
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
2025-07-21 15:46:30 +08:00
|
|
|
|
children: [
|
|
|
|
|
// 取消发送
|
|
|
|
|
Container(
|
|
|
|
|
height: 60.0,
|
|
|
|
|
width: 60.0,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
borderRadius: BorderRadius.circular(50.0),
|
|
|
|
|
color: voiceType == 2 ? Colors.red : Colors.black38,
|
|
|
|
|
),
|
|
|
|
|
child: Icon(
|
|
|
|
|
Icons.close,
|
|
|
|
|
color: Colors.white54,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
// 语音转文字
|
2025-08-21 10:50:38 +08:00
|
|
|
|
// Container(
|
|
|
|
|
// height: 60.0,
|
|
|
|
|
// width: 60.0,
|
|
|
|
|
// decoration: BoxDecoration(
|
|
|
|
|
// borderRadius: BorderRadius.circular(50.0),
|
|
|
|
|
// color: voiceType == 3 ? Color(0xFF89E45B) : Colors.black38,
|
|
|
|
|
// ),
|
|
|
|
|
// child: Icon(
|
|
|
|
|
// Icons.translate,
|
|
|
|
|
// color: Colors.white54,
|
|
|
|
|
// ),
|
|
|
|
|
// ),
|
2025-07-21 15:46:30 +08:00
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
// 语音转文字(识别结果状态)
|
|
|
|
|
Positioned(
|
|
|
|
|
bottom: 120,
|
|
|
|
|
left: 30,
|
|
|
|
|
right: 30,
|
|
|
|
|
child: Visibility(
|
|
|
|
|
visible: voiceToTransfer,
|
|
|
|
|
child: Column(
|
|
|
|
|
children: [
|
|
|
|
|
// 提示结果
|
|
|
|
|
Stack(
|
|
|
|
|
children: [
|
|
|
|
|
Container(
|
|
|
|
|
height: 100.0,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.red,
|
|
|
|
|
borderRadius: BorderRadius.circular(15.0),
|
|
|
|
|
),
|
|
|
|
|
child: const Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
children: [
|
|
|
|
|
Icon(
|
|
|
|
|
Icons.info_outlined,
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
),
|
|
|
|
|
Text(
|
|
|
|
|
'未识别到文字。',
|
|
|
|
|
style: TextStyle(color: Colors.white),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
Positioned(
|
|
|
|
|
right: 35.0,
|
|
|
|
|
bottom: 1,
|
|
|
|
|
child: RotatedBox(
|
|
|
|
|
quarterTurns: 0,
|
|
|
|
|
child: CustomPaint(painter: ArrowShape(arrowColor: Colors.red, arrowSize: 10.0)),
|
|
|
|
|
)),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(
|
|
|
|
|
height: 50.0,
|
|
|
|
|
),
|
|
|
|
|
// 操作项
|
|
|
|
|
Row(
|
2025-08-21 10:50:38 +08:00
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
2025-07-21 15:46:30 +08:00
|
|
|
|
children: [
|
|
|
|
|
GestureDetector(
|
|
|
|
|
child: Container(
|
|
|
|
|
height: 60.0,
|
|
|
|
|
width: 60.0,
|
|
|
|
|
decoration: const BoxDecoration(
|
|
|
|
|
color: Colors.transparent,
|
|
|
|
|
),
|
|
|
|
|
child: const Column(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
children: [
|
|
|
|
|
Icon(
|
|
|
|
|
Icons.undo,
|
|
|
|
|
color: Colors.white54,
|
|
|
|
|
),
|
|
|
|
|
Text(
|
|
|
|
|
'取消',
|
|
|
|
|
style: TextStyle(color: Colors.white70),
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
onTap: () {
|
|
|
|
|
setState(() {
|
|
|
|
|
voicePanelEnable = false;
|
|
|
|
|
voiceToTransfer = false;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
GestureDetector(
|
|
|
|
|
child: Container(
|
|
|
|
|
height: 60.0,
|
|
|
|
|
width: 100.0,
|
|
|
|
|
decoration: const BoxDecoration(
|
|
|
|
|
color: Colors.transparent,
|
|
|
|
|
),
|
|
|
|
|
child: const Column(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
children: [
|
|
|
|
|
Icon(
|
|
|
|
|
Icons.graphic_eq_rounded,
|
|
|
|
|
color: Colors.white54,
|
|
|
|
|
),
|
|
|
|
|
Text(
|
|
|
|
|
'发送原语音',
|
|
|
|
|
style: TextStyle(color: Colors.white70),
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
onTap: () {},
|
|
|
|
|
),
|
|
|
|
|
GestureDetector(
|
|
|
|
|
child: Container(
|
|
|
|
|
height: 60.0,
|
|
|
|
|
width: 60.0,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
borderRadius: BorderRadius.circular(50.0),
|
|
|
|
|
color: Colors.white12,
|
|
|
|
|
),
|
|
|
|
|
child: const Icon(
|
|
|
|
|
Icons.check,
|
|
|
|
|
color: Colors.white12,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
onTap: () {},
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
// 提示文字(操作状态)
|
|
|
|
|
Positioned(
|
|
|
|
|
bottom: 120,
|
|
|
|
|
left: 0,
|
|
|
|
|
width: MediaQuery.of(context).size.width,
|
|
|
|
|
child: Visibility(
|
|
|
|
|
visible: !voiceToTransfer,
|
|
|
|
|
child: Align(
|
|
|
|
|
child: Text(
|
|
|
|
|
voiceTypeMap[voiceType],
|
|
|
|
|
style: const TextStyle(color: Colors.white70),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
// 背景
|
|
|
|
|
Align(
|
|
|
|
|
alignment: Alignment.bottomCenter,
|
|
|
|
|
child: Visibility(
|
|
|
|
|
visible: !voiceToTransfer,
|
2025-08-21 10:50:38 +08:00
|
|
|
|
child: Image.asset(
|
|
|
|
|
'assets/images/voice_bg.webp',
|
|
|
|
|
width: double.infinity,
|
|
|
|
|
height: 100.0,
|
|
|
|
|
fit: BoxFit.fill,
|
|
|
|
|
),
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
// 背景图标
|
|
|
|
|
Positioned(
|
|
|
|
|
bottom: 25,
|
|
|
|
|
left: 0,
|
|
|
|
|
width: MediaQuery.of(context).size.width,
|
|
|
|
|
child: Visibility(
|
|
|
|
|
visible: !voiceToTransfer,
|
|
|
|
|
child: const Align(
|
|
|
|
|
child: Icon(
|
|
|
|
|
Icons.graphic_eq_rounded,
|
|
|
|
|
color: Colors.black54,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 渲染聊天消息公共部分
|
|
|
|
|
class RenderChatItem extends StatelessWidget {
|
|
|
|
|
const RenderChatItem({
|
|
|
|
|
super.key,
|
|
|
|
|
required this.data,
|
|
|
|
|
required this.child,
|
|
|
|
|
});
|
|
|
|
|
final V2TimMessage data; // 消息数据
|
|
|
|
|
final Widget? child; // 消息体
|
|
|
|
|
|
|
|
|
|
// 设置箭头颜色
|
|
|
|
|
// Color arrowColor(data) {
|
|
|
|
|
// Color color = Colors.transparent;
|
|
|
|
|
// if ([8].contains(data.elemType)) {
|
|
|
|
|
// // 红包箭头颜色
|
|
|
|
|
// color = const Color(0xFFFFA52F);
|
|
|
|
|
// } else if ([9].contains(data.elemType)) {
|
|
|
|
|
// // 位置箭头颜色
|
|
|
|
|
// color = const Color(0xFFFFFFFF);
|
|
|
|
|
// } else {
|
|
|
|
|
// color = !data['isme'] ? const Color(0xFFFFFFFF) : const Color(0xFF9543FF);
|
|
|
|
|
// }
|
|
|
|
|
// return color;
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2025-09-03 11:25:31 +08:00
|
|
|
|
String? displayName = (data.friendRemark?.isNotEmpty ?? false)
|
|
|
|
|
? data.friendRemark
|
|
|
|
|
: (data.nameCard?.isNotEmpty ?? false)
|
|
|
|
|
? data.nameCard
|
|
|
|
|
: (data.nickName?.isNotEmpty ?? false)
|
|
|
|
|
? data.nickName
|
|
|
|
|
: '未知昵称';
|
2025-07-21 15:46:30 +08:00
|
|
|
|
return Container(
|
|
|
|
|
margin: const EdgeInsets.only(bottom: 10.0),
|
|
|
|
|
child: Row(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
!(data.isSelf ?? false)
|
2025-09-03 11:25:31 +08:00
|
|
|
|
? GestureDetector(
|
|
|
|
|
onTap: () {
|
|
|
|
|
// 头像点击事件
|
|
|
|
|
logger.e("点击了头像");
|
|
|
|
|
Get.toNamed('/vloger', arguments: {'memberId': data.sender});
|
|
|
|
|
},
|
|
|
|
|
child: SizedBox(
|
|
|
|
|
height: 35.0,
|
|
|
|
|
width: 35.0,
|
|
|
|
|
child: ClipRRect(
|
|
|
|
|
borderRadius: const BorderRadius.all(Radius.circular(20.0)),
|
|
|
|
|
child: NetworkOrAssetImage(imageUrl: data.faceUrl),
|
|
|
|
|
),
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
: const SizedBox.shrink(),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: !(data.isSelf ?? false) ? const EdgeInsets.only(left: 10.0, right: 40.0) : const EdgeInsets.only(left: 40.0, right: 10.0),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: !(data.isSelf ?? false) ? CrossAxisAlignment.start : CrossAxisAlignment.end,
|
|
|
|
|
children: [
|
2025-09-03 11:25:31 +08:00
|
|
|
|
// Text(
|
|
|
|
|
// displayName ?? '未知昵称',
|
|
|
|
|
// style: const TextStyle(color: Colors.grey, fontSize: 12.0),
|
|
|
|
|
// ),
|
|
|
|
|
// const SizedBox(
|
|
|
|
|
// height: 3.0,
|
|
|
|
|
// ),
|
2025-07-21 15:46:30 +08:00
|
|
|
|
Stack(
|
|
|
|
|
children: [
|
|
|
|
|
// 气泡箭头
|
|
|
|
|
/* Visibility(
|
|
|
|
|
// 显示箭头(消息+语音+红包+位置)
|
|
|
|
|
visible: [3, 7, 8, 9].contains(data['contentType']),
|
|
|
|
|
child: Positioned(
|
|
|
|
|
left: !data['isme'] ? 1 : null,
|
|
|
|
|
right: data['isme'] ? 1 : null,
|
|
|
|
|
top: 20.0,
|
|
|
|
|
child: RotatedBox(
|
|
|
|
|
quarterTurns: !data['isme'] ? 1 : -1,
|
|
|
|
|
child: CustomPaint(painter: ArrowShape(arrowColor: arrowColor(data))),
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
), */
|
|
|
|
|
Container(
|
|
|
|
|
child: child,
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
data.isSelf ?? false
|
|
|
|
|
? SizedBox(
|
|
|
|
|
height: 35.0,
|
|
|
|
|
width: 35.0,
|
|
|
|
|
child: ClipRRect(
|
|
|
|
|
borderRadius: const BorderRadius.all(Radius.circular(20.0)),
|
2025-08-21 10:50:38 +08:00
|
|
|
|
child: NetworkOrAssetImage(imageUrl: data.faceUrl),
|
2025-07-21 15:46:30 +08:00
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
: const SizedBox.shrink(),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 绘制气泡箭头
|
|
|
|
|
class ArrowShape extends CustomPainter {
|
|
|
|
|
ArrowShape({
|
|
|
|
|
required this.arrowColor,
|
|
|
|
|
this.arrowSize = 7,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
final Color arrowColor; // 箭头颜色
|
|
|
|
|
final double arrowSize; // 箭头大小
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void paint(Canvas canvas, Size size) {
|
|
|
|
|
var paint = Paint()..color = arrowColor;
|
|
|
|
|
|
|
|
|
|
var path = Path();
|
|
|
|
|
path.lineTo(-arrowSize, 0);
|
|
|
|
|
path.lineTo(0, arrowSize);
|
|
|
|
|
path.lineTo(arrowSize, 0);
|
|
|
|
|
canvas.drawPath(path, paint);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
bool shouldRepaint(CustomPainter oldDelegate) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|