Browse Source

修改bug

ios
Jolie 3 months ago
parent
commit
336e4247a3
10 changed files with 1320 additions and 388 deletions
  1. 66
      lib/controller/message/chat_controller.dart
  2. 330
      lib/im/chat_presence_manager.dart
  3. 227
      lib/im/im_manager.dart
  4. 4
      lib/model/home/marriage_data.dart
  5. 4
      lib/model/mine/user_base_data.dart
  6. 178
      lib/pages/message/chat_page.dart
  7. 38
      lib/widget/message/chat_input_bar.dart
  8. 676
      lib/widget/message/image_item.dart
  9. 66
      lib/widget/message/more_options_view.dart
  10. 119
      lib/widget/message/voice_item.dart

66
lib/controller/message/chat_controller.dart

@ -3,6 +3,7 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'dart:convert';
import '../../im/im_manager.dart';
import '../../im/chat_presence_manager.dart';
import '../../model/home/marriage_data.dart';
import '../../model/live/gift_product_model.dart';
import '../../network/network_service.dart';
@ -21,6 +22,9 @@ class ChatController extends GetxController {
// 使
String? _externalNickName;
String? _externalAvatarUrl;
// 线
final RxBool isUserOnline = RxBool(false);
//
final RxList<EMMessage> messages = RxList<EMMessage>([]);
@ -31,6 +35,9 @@ class ChatController extends GetxController {
//
final RxBool isSendingVideo = RxBool(false);
final RxString sendingStatus = RxString('');
//
final RxBool isSendingVoice = RxBool(false);
//
final RxList<GiftProductModel> giftProducts = <GiftProductModel>[].obs;
@ -83,15 +90,55 @@ class ChatController extends GetxController {
//
fetchUserInfo();
fetchMessages();
fetchMessages().then((_) {
//
markAllMessagesAsRead();
});
//
loadGiftProducts();
// 线
subscribeUserPresence();
// UI
if (_externalNickName != null || _externalAvatarUrl != null) {
update();
}
}
/// 线
Future<void> subscribeUserPresence() async {
try {
await ChatPresenceManager().subscribeUserPresence(
userId: userId,
onStatusChanged: (isOnline) {
//
isUserOnline.value = isOnline;
if (Get.isLogEnable) {
Get.log('✅ [ChatController] 用户在线状态更新: userId=$userId, isOnline=$isOnline');
}
update();
},
);
} catch (e) {
if (Get.isLogEnable) {
Get.log('❌ [ChatController] 订阅用户在线状态异常: $e');
}
// 线
isUserOnline.value = false;
update();
}
}
/// 线
Future<void> unsubscribeUserPresence() async {
try {
await ChatPresenceManager().unsubscribeUserPresence(userId);
} catch (e) {
if (Get.isLogEnable) {
Get.log('❌ [ChatController] 取消订阅用户在线状态异常: $e');
}
}
}
/// ConversationController
void _loadUserInfoFromConversationCache() {
try {
@ -155,6 +202,8 @@ class ChatController extends GetxController {
@override
void onClose() {
// 线
unsubscribeUserPresence();
// ChatController
IMManager.instance.unregisterChatController(userId);
super.onClose();
@ -487,8 +536,20 @@ class ChatController extends GetxController {
///
Future<bool> sendVoiceMessage(String filePath, int seconds) async {
//
if (isSendingVoice.value) {
if (Get.isLogEnable) {
Get.log('⚠️ [ChatController] 语音消息正在发送中,忽略重复发送请求');
} else {
print('⚠️ [ChatController] 语音消息正在发送中,忽略重复发送请求');
}
return false;
}
EMMessage? tempMessage;
try {
isSendingVoice.value = true;
if (Get.isLogEnable) {
Get.log('📤 [ChatController] 准备发送语音消息: path=$filePath, duration=$seconds, targetUserId=$userId');
} else {
@ -552,6 +613,7 @@ class ChatController extends GetxController {
});
}
isSendingVoice.value = false;
return true;
} else {
//
@ -559,10 +621,12 @@ class ChatController extends GetxController {
if (index != -1) {
update();
}
isSendingVoice.value = false;
SmartDialog.showToast('语音发送失败,请点击重发');
return false;
}
} catch (e) {
isSendingVoice.value = false;
if (Get.isLogEnable) {
Get.log('发送语音消息失败: $e');
}

330
lib/im/chat_presence_manager.dart

@ -0,0 +1,330 @@
import 'package:im_flutter_sdk/im_flutter_sdk.dart';
/// 线
class ChatPresenceManager {
//
static final ChatPresenceManager _instance = ChatPresenceManager._internal();
factory ChatPresenceManager() => _instance;
ChatPresenceManager._internal();
// ID到在线状态的映射
final Map<String, bool> _onlineStatusMap = {};
// ID到EMPresence的映射
final Map<String, EMPresence> _presenceMap = {};
// ID集合
final Set<String> _listenerIds = {};
// 线
final Map<String, Function(bool)> _statusCallbacks = {};
//
final Map<String, Function(EMPresence)> _presenceCallbacks = {};
/// 线
/// [userId] ID
/// [onStatusChanged]
/// [onPresenceChanged]
Future<void> subscribeUserPresence({
required String userId,
Function(bool)? onStatusChanged,
Function(EMPresence)? onPresenceChanged,
}) async {
try {
//
if (onStatusChanged != null) {
_statusCallbacks[userId] = onStatusChanged;
}
if (onPresenceChanged != null) {
_presenceCallbacks[userId] = onPresenceChanged;
}
// 7
await EMClient.getInstance.presenceManager.subscribe(
members: [userId],
expiry: 604800,
);
// UI
await _fetchUserPresence(userId);
//
_setupPresenceListener(userId);
print('✅ [ChatPresenceManager] 订阅成功: userId=$userId');
} on EMError catch (e) {
print('订阅用户[$userId]在线状态失败: ${e.code}, ${e.description}');
rethrow;
}
}
/// 线
Future<void> subscribeUsersPresence({
required List<String> userIds,
Function(String, bool)? onStatusChanged,
Function(String, EMPresence)? onPresenceChanged,
}) async {
if (userIds.isEmpty) return;
try {
// 100
await EMClient.getInstance.presenceManager.subscribe(
members: userIds,
expiry: 604800,
);
//
for (final userId in userIds) {
if (onStatusChanged != null) {
_statusCallbacks[userId] = (isOnline) => onStatusChanged(userId, isOnline);
}
if (onPresenceChanged != null) {
_presenceCallbacks[userId] = (presence) => onPresenceChanged(userId, presence);
}
//
await _fetchUserPresence(userId);
}
//
_setupBatchPresenceListener();
} on EMError catch (e) {
print('批量订阅用户在线状态失败: ${e.code}, ${e.description}');
rethrow;
}
}
/// 线
bool isUserOnline(String userId) {
return _onlineStatusMap[userId] ?? false;
}
/// 线
EMPresence? getUserPresence(String userId) {
return _presenceMap[userId];
}
/// 线
String getStatusDescription(String userId) {
final presence = _presenceMap[userId];
if (presence == null) return '未知';
final statusDesc = presence.statusDescription?.toLowerCase() ?? '';
//
if (statusDesc.contains('online')) return '在线';
if (statusDesc.contains('away')) return '离开';
if (statusDesc.contains('busy')) return '忙碌';
if (statusDesc.contains('dnd')) return '勿扰';
if (statusDesc.contains('offline')) return '离线';
return '未知';
}
/// 线
int getStatusColor(String userId) {
final isOnline = _onlineStatusMap[userId] ?? false;
final presence = _presenceMap[userId];
if (!isOnline) return 0xFF999999; //
final statusDesc = presence?.statusDescription?.toLowerCase() ?? '';
if (statusDesc.contains('busy')) return 0xFFFF3B30; //
if (statusDesc.contains('away')) return 0xFFFF9500; //
if (statusDesc.contains('dnd')) return 0xFFAF52DE; //
if (statusDesc.contains('online')) return 0xFF34C759; // 绿
return 0xFF34C759; // 绿
}
/// 线
Future<void> refreshUserPresence(String userId) async {
try {
await _fetchUserPresence(userId);
} on EMError catch (e) {
print('刷新用户[$userId]在线状态失败: ${e.code}, ${e.description}');
rethrow;
}
}
/// 线
Future<void> unsubscribeUserPresence(String userId) async {
try {
//
await EMClient.getInstance.presenceManager.unsubscribe(members: [userId]);
//
_statusCallbacks.remove(userId);
_presenceCallbacks.remove(userId);
_onlineStatusMap.remove(userId);
_presenceMap.remove(userId);
} on EMError catch (e) {
print('取消订阅用户[$userId]在线状态失败: ${e.code}, ${e.description}');
rethrow;
}
}
///
Future<void> unsubscribeUsersPresence(List<String> userIds) async {
if (userIds.isEmpty) return;
try {
await EMClient.getInstance.presenceManager.unsubscribe(members: userIds);
for (final userId in userIds) {
_statusCallbacks.remove(userId);
_presenceCallbacks.remove(userId);
_onlineStatusMap.remove(userId);
_presenceMap.remove(userId);
}
} on EMError catch (e) {
print('批量取消订阅在线状态失败: ${e.code}, ${e.description}');
rethrow;
}
}
///
Future<void> dispose() async {
//
for (final listenerId in _listenerIds) {
try {
EMClient.getInstance.presenceManager.removeEventHandler(listenerId);
} catch (e) {
//
}
}
_listenerIds.clear();
//
_statusCallbacks.clear();
_presenceCallbacks.clear();
//
_onlineStatusMap.clear();
_presenceMap.clear();
}
/// 线
Future<void> _fetchUserPresence(String userId) async {
try {
List<EMPresence> presences = await EMClient.getInstance.presenceManager
.fetchPresenceStatus(members: [userId]);
if (presences.isNotEmpty) {
final presence = presences.first;
print('📥 [ChatPresenceManager] 获取到Presence状态: userId=$userId, statusDescription=${presence.statusDescription}');
_presenceMap[userId] = presence;
_updateOnlineStatus(userId, presence);
} else {
print('⚠️ [ChatPresenceManager] 未获取到Presence状态: userId=$userId');
}
} on EMError catch (e) {
print('获取用户[$userId]在线状态失败: ${e.code}, ${e.description}');
rethrow;
}
}
/// 线
void _updateOnlineStatus(String userId, EMPresence presence) {
final statusDesc = presence.statusDescription?.toLowerCase() ?? '';
final oldStatus = _onlineStatusMap[userId] ?? false;
bool newStatus;
// 线
if (statusDesc.contains('online') || statusDesc.contains('available')) {
newStatus = true;
} else if (statusDesc.contains('offline')) {
newStatus = false;
} else {
// awaybusydnd "在线"
newStatus = true;
}
//
final isFirstTime = !_onlineStatusMap.containsKey(userId);
//
_onlineStatusMap[userId] = newStatus;
//
print('🔄 [ChatPresenceManager] 更新在线状态: userId=$userId, statusDescription=$statusDesc, oldStatus=$oldStatus, newStatus=$newStatus, isFirstTime=$isFirstTime');
//
if (isFirstTime || oldStatus != newStatus) {
print('✅ [ChatPresenceManager] 触发状态回调: userId=$userId, isOnline=$newStatus');
_statusCallbacks[userId]?.call(newStatus);
}
//
_presenceCallbacks[userId]?.call(presence);
}
/// 线
void _setupPresenceListener(String userId) {
final listenerId = 'presence_$userId';
if (_listenerIds.contains(listenerId)) return;
EMClient.getInstance.presenceManager.addEventHandler(
listenerId,
EMPresenceEventHandler(
onPresenceStatusChanged: (list) {
print('📡 [ChatPresenceManager] 收到Presence状态变化通知: userId=$userId, 变化数量=${list.length}');
for (var presence in list) {
if (presence.publisher == userId) {
print('✅ [ChatPresenceManager] 匹配到目标用户: userId=$userId');
_presenceMap[userId] = presence;
_updateOnlineStatus(userId, presence);
break;
}
}
},
),
);
_listenerIds.add(listenerId);
}
///
void _setupBatchPresenceListener() {
const listenerId = 'presence_batch_listener';
if (_listenerIds.contains(listenerId)) return;
EMClient.getInstance.presenceManager.addEventHandler(
listenerId,
EMPresenceEventHandler(
onPresenceStatusChanged: (list) {
for (var presence in list) {
final userId = presence.publisher;
if (userId != null && _statusCallbacks.containsKey(userId)) {
_presenceMap[userId] = presence;
_updateOnlineStatus(userId, presence);
}
}
},
),
);
_listenerIds.add(listenerId);
}
///
Future<void> _refreshAllSubscribedUsers() async {
final userIds = _statusCallbacks.keys.toList();
if (userIds.isEmpty) return;
try {
List<EMPresence> presences = await EMClient.getInstance.presenceManager
.fetchPresenceStatus(members: userIds);
for (var presence in presences) {
final publisherId = presence.publisher;
if (publisherId != null && publisherId.isNotEmpty) {
_presenceMap[publisherId] = presence;
_updateOnlineStatus(publisherId, presence);
}
}
} on EMError catch (e) {
print('刷新所有订阅用户状态失败: ${e.code}, ${e.description}');
}
}
}

227
lib/im/im_manager.dart

@ -4,9 +4,11 @@ import 'package:get_storage/get_storage.dart';
import 'package:im_flutter_sdk/im_flutter_sdk.dart';
import 'package:video_thumbnail/video_thumbnail.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import '../controller/message/conversation_controller.dart';
import '../controller/message/chat_controller.dart';
import '../controller/global.dart';
import '../pages/mine/login_page.dart';
// IM管理器实现使SDK类型和方法
class IMManager {
@ -24,9 +26,13 @@ class IMManager {
//
static const String _connectionHandlerKey = 'im_manager_connection_handler';
static const String _chatHandlerKey = 'im_manager_chat_handler';
static const String _presenceHandlerKey = 'im_manager_presence_handler';
// ChatController key userId
final Map<String, ChatController> _activeChatControllers = {};
// Presence key userId
final Map<String, Function(bool)> _presenceCallbacks = {};
IMManager._internal() {
print('IMManager instance created');
@ -96,8 +102,11 @@ class IMManager {
onUserKickedByOtherDevice: () {
if (Get.isLogEnable) {
Get.log('⚠️ [IMManager] 用户被其他设备踢出');
} else {
print('⚠️ [IMManager] 用户被其他设备踢出');
}
// TODO:
// 线
_handleUserKickedOffline();
},
),
);
@ -229,6 +238,52 @@ class IMManager {
}
},
));
// Presence
EMClient.getInstance.presenceManager.addEventHandler(
_presenceHandlerKey,
EMPresenceEventHandler(
onPresenceStatusChanged: (List<EMPresence> presences) {
if (Get.isLogEnable) {
Get.log('📡 [IMManager] 收到 Presence 状态变化: ${presences.length} 个用户');
}
//
for (var presence in presences) {
final userId = presence.publisher;
if (userId != null && userId.isNotEmpty) {
// 使 statusDescription 线
final statusDesc = presence.statusDescription?.toLowerCase() ?? '';
// 线onlineavailable 线
final isOnline = statusDesc == 'online' || statusDesc == 'available';
if (Get.isLogEnable) {
Get.log('📡 [IMManager] Presence状态变化: userId=$userId, statusDescription=$statusDesc, isOnline=$isOnline');
} else {
print('📡 [IMManager] Presence状态变化: userId=$userId, statusDescription=$statusDesc, isOnline=$isOnline');
}
//
final callback = _presenceCallbacks[userId];
if (callback != null) {
callback(isOnline);
}
// ChatController
final controller = _activeChatControllers[userId];
if (controller != null) {
controller.isUserOnline.value = isOnline;
controller.update();
}
if (Get.isLogEnable) {
Get.log('✅ [IMManager] 用户在线状态更新: userId=$userId, isOnline=$isOnline');
}
}
}
},
),
);
_listenersRegistered = true;
if (Get.isLogEnable) {
Get.log('✅ [IMManager] 监听器注册成功');
@ -291,6 +346,45 @@ class IMManager {
}
}
/// 线
void _handleUserKickedOffline() async {
try {
//
SmartDialog.showToast('您的账号在其他设备登录,已被强制下线');
// 退 IM
await logout();
//
if (Get.isRegistered<ConversationController>()) {
final conversationController = Get.find<ConversationController>();
conversationController.clearConversations();
}
//
GetStorage().erase();
//
GlobalData().logout();
//
await Future.delayed(Duration(milliseconds: 500));
//
Get.offAll(() => LoginPage());
if (Get.isLogEnable) {
Get.log('✅ [IMManager] 用户被踢下线处理完成,已跳转到登录页');
}
} catch (e) {
if (Get.isLogEnable) {
Get.log('❌ [IMManager] 处理踢下线失败: $e');
} else {
print('❌ [IMManager] 处理踢下线失败: $e');
}
}
}
///
bool get isLoggedIn => _isLoggedIn;
@ -580,6 +674,137 @@ class IMManager {
]);
}
/// 线
/// [userId] ID
/// [callback]
Future<bool> subscribeUserPresence(String userId, Function(bool isOnline) callback) async {
try {
if (!_isLoggedIn) {
if (Get.isLogEnable) {
Get.log('⚠️ [IMManager] IM未登录,无法订阅在线状态');
}
return false;
}
//
_presenceCallbacks[userId] = callback;
final presenceManager = EMClient.getInstance.presenceManager;
// 7
await presenceManager.subscribe(
members: [userId],
expiry: 7 * 24 * 60 * 60, // 7
);
if (Get.isLogEnable) {
Get.log('✅ [IMManager] 已订阅用户在线状态: userId=$userId');
}
//
final onlineStatus = await getUserPresenceStatus(userId);
if (onlineStatus != null) {
if (Get.isLogEnable) {
Get.log('✅ [IMManager] 订阅后立即获取状态: userId=$userId, isOnline=$onlineStatus');
} else {
print('✅ [IMManager] 订阅后立即获取状态: userId=$userId, isOnline=$onlineStatus');
}
callback(onlineStatus);
} else {
if (Get.isLogEnable) {
Get.log('⚠️ [IMManager] 订阅后获取状态失败: userId=$userId');
} else {
print('⚠️ [IMManager] 订阅后获取状态失败: userId=$userId');
}
}
return true;
} catch (e) {
if (Get.isLogEnable) {
Get.log('❌ [IMManager] 订阅用户在线状态失败: $userId, 错误: $e');
} else {
print('❌ [IMManager] 订阅用户在线状态失败: $userId, 错误: $e');
}
return false;
}
}
/// 线
/// [userId] ID
Future<bool> unsubscribeUserPresence(String userId) async {
try {
if (!_isLoggedIn) {
return false;
}
//
_presenceCallbacks.remove(userId);
final presenceManager = EMClient.getInstance.presenceManager;
await presenceManager.unsubscribe(members: [userId]);
if (Get.isLogEnable) {
Get.log('✅ [IMManager] 已取消订阅用户在线状态: userId=$userId');
}
return true;
} catch (e) {
if (Get.isLogEnable) {
Get.log('❌ [IMManager] 取消订阅用户在线状态失败: $userId, 错误: $e');
}
return false;
}
}
/// 线
/// [userId] ID
/// true 线false 线null
Future<bool?> getUserPresenceStatus(String userId) async {
try {
if (!_isLoggedIn) {
if (Get.isLogEnable) {
Get.log('⚠️ [IMManager] IM未登录,无法获取在线状态');
}
return null;
}
final presenceManager = EMClient.getInstance.presenceManager;
// 线
final presences = await presenceManager.fetchPresenceStatus(
members: [userId],
);
if (presences.isEmpty) {
if (Get.isLogEnable) {
Get.log('⚠️ [IMManager] 未获取到用户在线状态: $userId');
}
return null;
}
final presence = presences.first;
// 使 statusDescription 线
final statusDesc = presence.statusDescription?.toLowerCase() ?? '';
// 线onlineavailable 线
final isOnline = statusDesc == 'online' || statusDesc == 'available';
if (Get.isLogEnable) {
Get.log('✅ [IMManager] 获取用户在线状态: userId=$userId, statusDescription=$statusDesc, isOnline=$isOnline');
} else {
print('✅ [IMManager] 获取用户在线状态: userId=$userId, statusDescription=$statusDesc, isOnline=$isOnline');
}
return isOnline;
} catch (e) {
if (Get.isLogEnable) {
Get.log('❌ [IMManager] 获取用户在线状态失败: $userId, 错误: $e');
} else {
print('❌ [IMManager] 获取用户在线状态失败: $userId, 错误: $e');
}
return null;
}
}
///
Future<List<EMMessage?>> getMessages(
String conversationId, {

4
lib/model/home/marriage_data.dart

@ -49,8 +49,8 @@ class MarriageData {
factory MarriageData.fromJson(Map<String, dynamic> json) {
return MarriageData(
miId: json['miId'] ?? '',
userId: json['userId'] ?? '',
miId: '1180578682436194304',//json['miId'] ?? '',
userId: '1114267797208305664',//json['userId'] ?? '',
profilePhoto: json['profilePhoto'] ?? '',
nickName: json['nickName'] ?? '',
isRealNameCertified: json['isRealNameCertified'] ?? false,

4
lib/model/mine/user_base_data.dart

@ -7,6 +7,7 @@ class UserBaseData {
final String realName;
final String userId;
final int matchmakerType;
final bool? isOnline; // 线
UserBaseData({
required this.matchmakerFlag,
@ -16,6 +17,7 @@ class UserBaseData {
required this.realName,
required this.userId,
required this.matchmakerType,
this.isOnline,
});
// JSON映射创建实例
@ -28,6 +30,7 @@ class UserBaseData {
phone: json['phone'] ?? '',
realName: json['realName'] ?? '',
userId: json['userId'] ?? '',
isOnline: json['isOnline'] as bool?,
);
}
@ -41,6 +44,7 @@ class UserBaseData {
'phone': phone,
'realName': realName,
'userId': userId,
'isOnline': isOnline,
};
}

178
lib/pages/message/chat_page.dart

@ -82,6 +82,13 @@ class _ChatPageState extends State<ChatPage> {
}
});
});
//
WidgetsBinding.instance.addPostFrameCallback((_) {
Future.delayed(Duration(milliseconds: 300), () {
_controller.markAllMessagesAsRead();
});
});
}
@override
@ -92,113 +99,6 @@ class _ChatPageState extends State<ChatPage> {
super.dispose();
}
// 线
bool _isUserOnline(ChatController controller) {
try {
// userData
// 线线
if (widget.userData != null) {
// userData中读取在线状态的逻辑
// 线true
return true;
}
// userData线
//
final receiveMessages = controller.messages
.where((msg) => msg.direction == MessageDirection.RECEIVE)
.toList();
if (receiveMessages.isNotEmpty) {
//
final lastMessage = receiveMessages.last;
// 线
Map<String, dynamic>? attributes;
try {
attributes = lastMessage.attributes;
} catch (e) {
return false;
}
if (attributes != null && attributes.isNotEmpty) {
// sender_isOnline
final isOnlineStr = attributes['sender_isOnline'] as String?;
if (isOnlineStr == 'true') {
// 5线
final lastActiveTimeStr = attributes['sender_lastActiveTime'] as String?;
if (lastActiveTimeStr != null) {
try {
final lastActiveTime = int.parse(lastActiveTimeStr);
final now = DateTime.now().millisecondsSinceEpoch;
final diff = now - lastActiveTime;
// 5线5 * 60 * 1000
return diff < 5 * 60 * 1000;
} catch (e) {
// 使 isOnline
return true;
}
} else {
return true;
}
}
}
}
//
// 线
if (controller.messages.isNotEmpty) {
//
final sortedMessages = List<EMMessage>.from(controller.messages)
..sort((a, b) {
final timeA = a.serverTime;
final timeB = b.serverTime;
return timeB.compareTo(timeA);
});
for (final message in sortedMessages) {
Map<String, dynamic>? attributes;
try {
attributes = message.attributes;
} catch (e) {
continue;
}
if (attributes != null && attributes.isNotEmpty) {
// 线
final senderIsOnline = attributes['sender_isOnline'] as String?;
final receiverIsOnline = attributes['receiver_isOnline'] as String?;
final isOnlineStr = senderIsOnline ?? receiverIsOnline;
if (isOnlineStr == 'true') {
final senderLastActiveTime = attributes['sender_lastActiveTime'] as String?;
final receiverLastActiveTime = attributes['receiver_lastActiveTime'] as String?;
final lastActiveTimeStr = senderLastActiveTime ?? receiverLastActiveTime;
if (lastActiveTimeStr != null) {
try {
final lastActiveTime = int.parse(lastActiveTimeStr);
final now = DateTime.now().millisecondsSinceEpoch;
final diff = now - lastActiveTime;
// 5线
if (diff < 5 * 60 * 1000) {
return true;
}
} catch (e) {
return true;
}
} else {
return true;
}
}
}
}
}
return false;
} catch (e) {
return false;
}
}
//
void _showGiftPopup() {
final giftProducts = _controller.giftProducts.toList();
@ -252,28 +152,36 @@ class _ChatPageState extends State<ChatPage> {
controller.userNickName ?? widget.userData?.nickName ?? '',
style: TextStyle(fontSize: 18.sp),
),
if (_isUserOnline(controller))
SizedBox(width: 8.w),
// 线线
if (_isUserOnline(controller))
Container(
width: 46.w,
height: 26.h,
// padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 1.h),
decoration: BoxDecoration(
color: Color.fromRGBO(234, 255, 219, 0.5),
borderRadius: BorderRadius.circular(178),
),
child: Center(
child: Text(
'在线',
style: TextStyle(
fontSize: 12.sp,
color: Color.fromRGBO(38, 199, 124, 1),
// 使 Obx 线
Obx(() {
final isOnline = controller.isUserOnline.value;
if (isOnline) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(width: 8.w),
Container(
width: 46.w,
height: 26.h,
decoration: BoxDecoration(
color: Color.fromRGBO(234, 255, 219, 0.5),
borderRadius: BorderRadius.circular(178),
),
child: Center(
child: Text(
'在线',
style: TextStyle(
fontSize: 12.sp,
color: Color.fromRGBO(38, 199, 124, 1),
),
),
),
),
),
),
),
],
);
}
return SizedBox.shrink();
}),
],
),
centerTitle: false,
@ -374,20 +282,6 @@ class _ChatPageState extends State<ChatPage> {
//
await controller.sendVoiceMessage(filePath, seconds);
},
onVideoRecorded: (filePath, duration) async {
print('🎬 [ChatPage] 收到视频录制/选择回调');
print('文件路径: $filePath');
print('时长: $duration');
//
if (controller.isSendingVideo.value) {
print('⚠️ [ChatPage] 视频正在发送中,忽略新的发送请求');
return;
}
// /
await controller.sendVideoMessage(filePath, duration);
},
onGiftTap: () {
//
_showGiftPopup();

38
lib/widget/message/chat_input_bar.dart

@ -7,13 +7,11 @@ import '../../config/emoji_config.dart';
import '../emoji_panel.dart';
import 'more_options_view.dart';
import 'voice_input_view.dart';
import 'video_input_view.dart';
class ChatInputBar extends StatefulWidget {
final ValueChanged<String> onSendMessage;
final ValueChanged<List<String>>? onImageSelected;
final Function(String filePath, int seconds)? onVoiceRecorded;
final Function(String filePath, int duration)? onVideoRecorded;
final VoidCallback? onVoiceCall; //
final VoidCallback? onVideoCall; //
final VoidCallback? onGiftTap; //
@ -22,7 +20,6 @@ class ChatInputBar extends StatefulWidget {
required this.onSendMessage,
this.onImageSelected,
this.onVoiceRecorded,
this.onVideoRecorded,
this.onVoiceCall,
this.onVideoCall,
this.onGiftTap,
@ -38,7 +35,6 @@ class _ChatInputBarState extends State<ChatInputBar> {
final FocusNode _focusNode = FocusNode();
bool _isMoreOptionsVisible = false;
bool _isVoiceVisible = false;
bool _isVideoVisible = false;
bool _isEmojiVisible = false;
void _handleSendMessage() {
@ -55,7 +51,6 @@ class _ChatInputBarState extends State<ChatInputBar> {
_isMoreOptionsVisible = !_isMoreOptionsVisible;
if (_isMoreOptionsVisible) {
_isVoiceVisible = false;
_isVideoVisible = false;
_isEmojiVisible = false;
}
//
@ -82,34 +77,18 @@ class _ChatInputBarState extends State<ChatInputBar> {
_isVoiceVisible = !_isVoiceVisible;
if (_isVoiceVisible) {
_isMoreOptionsVisible = false;
_isVideoVisible = false;
_isEmojiVisible = false;
}
FocusManager.instance.primaryFocus?.unfocus();
});
}
void _toggleVideoOptions() {
print('🎬 [ChatInputBar] 视频按钮被点击,当前状态: $_isVideoVisible');
setState(() {
_isVideoVisible = !_isVideoVisible;
if (_isVideoVisible) {
_isMoreOptionsVisible = false;
_isVoiceVisible = false;
_isEmojiVisible = false;
}
FocusManager.instance.primaryFocus?.unfocus();
});
print('🎬 [ChatInputBar] 视频面板状态改变为: $_isVideoVisible');
}
void _toggleEmojiPanel() {
setState(() {
_isEmojiVisible = !_isEmojiVisible;
if (_isEmojiVisible) {
_isMoreOptionsVisible = false;
_isVoiceVisible = false;
_isVideoVisible = false;
}
FocusManager.instance.primaryFocus?.unfocus();
});
@ -118,11 +97,10 @@ class _ChatInputBarState extends State<ChatInputBar> {
//
void _closeAllPanels() {
if (!mounted) return;
if (_isMoreOptionsVisible || _isVoiceVisible || _isVideoVisible || _isEmojiVisible) {
if (_isMoreOptionsVisible || _isVoiceVisible || _isEmojiVisible) {
setState(() {
_isMoreOptionsVisible = false;
_isVoiceVisible = false;
_isVideoVisible = false;
_isEmojiVisible = false;
});
}
@ -313,12 +291,6 @@ class _ChatInputBarState extends State<ChatInputBar> {
width: 24.w,
height: 24.w,
).onTap(_toggleVoiceOptions),
//
Image.asset(
Assets.imagesVideo,
width: 24.w,
height: 24.w,
).onTap(_toggleVideoOptions),
//
// Image.asset(
// Assets.imagesSendCall,
@ -352,23 +324,17 @@ class _ChatInputBarState extends State<ChatInputBar> {
],
),
),
//
//
MoreOptionsView(
isVisible: _isMoreOptionsVisible,
onImageSelected: _handleImageTap,
onCameraSelected: _handleCameraTap,
onVideoSelected: widget.onVideoRecorded,
),
// MoreOptionsView
VoiceInputView(
isVisible: _isVoiceVisible,
onVoiceRecorded: widget.onVoiceRecorded,
),
//
VideoInputView(
isVisible: _isVideoVisible,
onVideoRecorded: widget.onVideoRecorded,
),
//
EmojiPanel(
isVisible: _isEmojiVisible,

676
lib/widget/message/image_item.dart

@ -1,18 +1,19 @@
import 'dart:io';
import 'package:dating_touchme_app/generated/assets.dart';
import 'package:dating_touchme_app/pages/message/image_viewer_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:im_flutter_sdk/im_flutter_sdk.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:dating_touchme_app/generated/assets.dart';
import 'package:dating_touchme_app/pages/message/image_viewer_page.dart';
class ImageItem extends StatelessWidget {
class ImageItem extends StatefulWidget {
final EMImageMessageBody imageBody;
final bool isSentByMe;
final bool showTime;
final String formattedTime;
final EMMessage message; //
final VoidCallback? onResend; //
final EMMessage message;
final VoidCallback? onResend;
const ImageItem({
required this.imageBody,
@ -24,56 +25,263 @@ class ImageItem extends StatelessWidget {
super.key,
});
@override
State<ImageItem> createState() => _ImageItemState();
}
class _ImageItemState extends State<ImageItem> {
bool _isDownloading = false;
bool _hasError = false;
String? _currentLocalPath;
String? _currentRemotePath;
bool _shouldHideProgressStatus = false; // PROGRESS状态
@override
void initState() {
super.initState();
_initImagePaths();
_checkAndDownloadImage();
//
if (widget.isSentByMe) {
//
_checkRemotePathAndUpdateStatus();
//
Future.delayed(Duration(milliseconds: 500), () {
if (mounted) {
_initImagePaths();
_checkRemotePathAndUpdateStatus();
setState(() {});
}
});
// 1PROGRESSloading状态
Future.delayed(Duration(seconds: 1), () {
if (mounted) {
_checkRemotePathAndUpdateStatus();
}
});
// 2PROGRESSloading状态
Future.delayed(Duration(seconds: 2), () {
if (mounted) {
_checkRemotePathAndUpdateStatus();
}
});
}
}
//
void _checkRemotePathAndUpdateStatus() {
// SDK已经更新
final body = widget.message.body;
if (body is EMImageMessageBody) {
final remotePath = body.remotePath;
// 使PROGRESS也隐藏loading
if (remotePath != null && remotePath.isNotEmpty) {
if (!_shouldHideProgressStatus) {
print('✅ 发送方:检测到远程路径,隐藏loading状态: $remotePath');
setState(() {
_shouldHideProgressStatus = true;
_currentRemotePath = remotePath; //
});
}
}
}
}
void _initImagePaths() {
// 使imageBody
//
if (widget.isSentByMe) {
_currentLocalPath = widget.imageBody.localPath;
_currentRemotePath = widget.imageBody.remotePath;
//
if ((_currentLocalPath == null || _currentLocalPath!.isEmpty) &&
widget.message.body is EMImageMessageBody) {
final body = widget.message.body as EMImageMessageBody;
_currentLocalPath = body.localPath;
_currentRemotePath = body.remotePath;
}
} else {
// 使
final body = widget.message.body;
if (body is EMImageMessageBody) {
_currentLocalPath = body.localPath;
_currentRemotePath = body.remotePath;
} else {
// 使imageBody
_currentLocalPath = widget.imageBody.localPath;
_currentRemotePath = widget.imageBody.remotePath;
}
}
print('📸 图片路径初始化:');
print(' 本地路径: $_currentLocalPath');
print(' 远程路径: $_currentRemotePath');
print(' 是否发送方: ${widget.isSentByMe}');
}
Future<void> _checkAndDownloadImage() async {
//
if (widget.isSentByMe) return;
//
if (_currentLocalPath != null && _currentLocalPath!.isNotEmpty) {
final file = File(_currentLocalPath!);
if (await file.exists()) {
final size = await file.length();
if (size > 1024) { // 1KB才认为有效
print('✅ 本地图片已存在且有效: $_currentLocalPath, 大小: ${size/1024}KB');
return;
} else {
print('⚠️ 本地图片文件太小或无效: $_currentLocalPath, 大小: $size bytes');
}
} else {
print('⚠️ 本地图片文件不存在: $_currentLocalPath');
}
}
//
if (_currentRemotePath != null && _currentRemotePath!.isNotEmpty) {
if (!_isDownloading) {
await _downloadImage();
}
} else {
print('❌ 没有可用的远程图片路径');
setState(() {
_hasError = true;
});
}
}
Future<void> _downloadImage() async {
if (_isDownloading) return;
setState(() {
_isDownloading = true;
_hasError = false;
});
try {
print('📥 开始下载图片消息: ${widget.message.msgId}');
// 使SDK下载图片downloadAttachment void
await EMClient.getInstance.chatManager
.downloadAttachment(widget.message);
//
await Future.delayed(Duration(milliseconds: 300));
// body
final body = widget.message.body;
if (body is EMImageMessageBody) {
setState(() {
_currentLocalPath = body.localPath;
_currentRemotePath = body.remotePath;
});
print('✅ 图片下载完成: $_currentLocalPath');
//
if (_currentLocalPath != null && _currentLocalPath!.isNotEmpty) {
final file = File(_currentLocalPath!);
if (await file.exists()) {
final size = await file.length();
print('✅ 文件验证成功,大小: ${size/1024}KB');
} else {
print('⚠️ 下载后文件不存在: $_currentLocalPath');
setState(() {
_hasError = true;
});
}
} else {
print('⚠️ 下载后本地路径为空');
setState(() {
_hasError = true;
});
}
} else {
print('❌ 消息体类型错误: ${body.runtimeType}');
setState(() {
_hasError = true;
});
}
} catch (e) {
print('❌ 下载图片失败: $e');
SmartDialog.showToast('图片下载失败');
setState(() {
_hasError = true;
});
} finally {
if (mounted) {
setState(() {
_isDownloading = false;
});
}
}
}
void _retryDownload() {
if (!_isDownloading) {
_downloadImage();
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
//
if (showTime) _buildTimeLabel(),
if (widget.showTime) _buildTimeLabel(),
Container(
padding: EdgeInsets.symmetric(
horizontal: 16.w,
vertical: 8.h,
),
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
child: Builder(
builder: (context) {
//
double maxWidth = 180.w;
double width = maxWidth;
double height = width * (304 / 289); // 289:304
final imageHeight = height + 10.h; // margin top (10.h)
double height = width * (304 / 289);
final imageHeight = height + 10.h;
return Row(
mainAxisAlignment: isSentByMe ? MainAxisAlignment.end : MainAxisAlignment.start,
mainAxisAlignment: widget.isSentByMe
? MainAxisAlignment.end
: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isSentByMe) _buildAvatar(),
if (!isSentByMe) SizedBox(width: 8.w),
//
if (isSentByMe)
if (!widget.isSentByMe) _buildAvatar(),
if (!widget.isSentByMe) SizedBox(width: 8.w),
//
if (widget.isSentByMe)
SizedBox(
height: imageHeight,
child: Center(
child: _buildMessageStatus(),
),
),
if (isSentByMe) SizedBox(width: 10.w),
if (widget.isSentByMe) SizedBox(width: 10.w),
//
GestureDetector(
onTap: _onImageTap,
child: Container(
margin: EdgeInsets.only(top: 10.h),
decoration: BoxDecoration(
color: isSentByMe ? Color(0xff8E7BF6) : Colors.white,
color: widget.isSentByMe ? Color(0xff8E7BF6) : Colors.white,
borderRadius: BorderRadius.circular(18.w),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(18.w),
child: _buildImage(),
child: _buildImageContent(width, height),
),
),
),
if (isSentByMe) SizedBox(width: 8.w),
if (isSentByMe) _buildAvatar(),
if (widget.isSentByMe) SizedBox(width: 8.w),
if (widget.isSentByMe) _buildAvatar(),
],
);
},
@ -83,88 +291,181 @@ class ImageItem extends StatelessWidget {
);
}
//
Widget _buildTimeLabel() {
return Container(
alignment: Alignment.center,
padding: EdgeInsets.symmetric(
horizontal: 16.w,
),
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 12.w,
),
child: Text(
formattedTime,
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey,
),
),
),
);
//
Widget _buildImageContent(double width, double height) {
//
if (widget.isSentByMe) {
//
return _buildSenderImage(width, height);
}
//
//
if (_hasError && !_isDownloading) {
return _buildErrorState(width, height);
}
//
if (_isDownloading) {
return _buildDownloadingState(width, height);
}
//
if (_currentLocalPath != null && _currentLocalPath!.isNotEmpty) {
return _buildLocalImage(width, height);
}
//
if (_currentRemotePath != null && _currentRemotePath!.isNotEmpty) {
return _buildNetworkImage(width, height);
}
//
return _buildErrorState(width, height);
}
//
Widget _buildAvatar() {
return Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.w),
image: DecorationImage(
image: AssetImage(Assets.imagesAvatarsExample),
//
Widget _buildSenderImage(double width, double height) {
// 1.
if (widget.imageBody.localPath.isNotEmpty) {
final file = File(widget.imageBody.localPath);
if (file.existsSync()) {
print('✅ 发送方使用原始本地路径: ${widget.imageBody.localPath}');
return Image.file(
file,
width: width,
height: height,
fit: BoxFit.cover,
),
),
);
}
//
Widget _buildImage() {
// 289x304比例计算图片尺寸
// 289
double maxWidth = 180.w;
double width = maxWidth;
double height = width * (304 / 289); // 289:304
//
final localPath = imageBody.localPath;
if (localPath.isNotEmpty) {
return Image.file(
File(localPath),
width: width,
height: height,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _buildErrorContainer(width, height);
},
);
errorBuilder: (context, error, stackTrace) {
print('❌ 发送方加载本地图片失败: $error');
//
return _buildSenderPlaceholder(width, height);
},
);
} else {
print('⚠️ 发送方原始本地路径文件不存在: ${widget.imageBody.localPath}');
}
}
//
final remotePath = imageBody.remotePath;
if (remotePath != null && remotePath.isNotEmpty) {
// 2.
if (_currentLocalPath != null && _currentLocalPath!.isNotEmpty) {
final file = File(_currentLocalPath!);
if (file.existsSync()) {
print('✅ 发送方使用当前本地路径: $_currentLocalPath');
return Image.file(
file,
width: width,
height: height,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
print('❌ 发送方加载当前本地图片失败: $error');
return _buildSenderPlaceholder(width, height);
},
);
} else {
print('⚠️ 发送方当前本地路径文件不存在: $_currentLocalPath');
}
}
// 3. loading
if (_currentRemotePath != null && _currentRemotePath!.isNotEmpty) {
print('🌐 发送方使用远程路径: $_currentRemotePath');
// loading状态loading
return Image.network(
remotePath,
_currentRemotePath!,
width: width,
height: height,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return _buildLoadingContainer(width, height);
if (loadingProgress == null) {
print('✅ 发送方网络图片加载完成');
return child;
}
// loading状态loading
if (_shouldHideProgressStatus) {
print('⏭️ 发送方:已超时,跳过网络图片loading状态');
return _buildSenderPlaceholder(width, height);
}
// loading
return _buildLoadingState(width, height);
},
errorBuilder: (context, error, stackTrace) {
return _buildErrorContainer(width, height);
print('❌ 发送方加载网络图片失败: $error');
//
return _buildSenderPlaceholder(width, height);
},
);
}
// 4.
print('⚠️ 发送方:所有路径都不可用,显示占位符');
return _buildSenderPlaceholder(width, height);
}
//
Widget _buildSenderPlaceholder(double width, double height) {
return Container(
width: width,
height: height,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18.w),
color: Color(0xff8E7BF6).withOpacity(0.1),
),
child: Center(
child: Icon(
Icons.image,
size: 32.w,
color: Color(0xff8E7BF6).withOpacity(0.5),
),
),
);
}
//
Widget _buildLocalImage(double width, double height) {
try {
final file = File(_currentLocalPath!);
if (file.existsSync()) {
return Image.file(
file,
width: width,
height: height,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
print('❌ 加载本地图片失败: $error');
return _buildErrorState(width, height);
},
);
} else {
print('❌ 本地图片文件不存在: $_currentLocalPath');
return _buildErrorState(width, height);
}
} catch (e) {
print('❌ 加载本地图片异常: $e');
return _buildErrorState(width, height);
}
}
//
return _buildErrorContainer(width, height);
//
Widget _buildNetworkImage(double width, double height) {
return Image.network(
_currentRemotePath!,
width: width,
height: height,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return _buildLoadingState(width, height);
},
errorBuilder: (context, error, stackTrace) {
print('❌ 加载网络图片失败: $error');
return _buildErrorState(width, height);
},
);
}
//
Widget _buildLoadingContainer(double width, double height) {
//
Widget _buildLoadingState(double width, double height) {
return Container(
width: width,
height: height,
@ -175,28 +476,131 @@ class ImageItem extends StatelessWidget {
child: Center(
child: CircularProgressIndicator(
strokeWidth: 2.w,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.grey[400]!,
valueColor: AlwaysStoppedAnimation<Color>(Colors.grey[400]!),
),
),
);
}
//
Widget _buildDownloadingState(double width, double height) {
return Container(
width: width,
height: height,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18.w),
color: Colors.grey[200],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
strokeWidth: 2.w,
valueColor: AlwaysStoppedAnimation<Color>(Color(0xff8E7BF6)),
),
SizedBox(height: 8.h),
Text(
'下载中...',
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[600],
),
),
],
),
);
}
//
Widget _buildErrorState(double width, double height) {
return Container(
width: width,
height: height,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18.w),
color: Colors.grey[200],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.broken_image,
size: 32.w,
color: Colors.grey[400],
),
SizedBox(height: 8.h),
Text(
'图片加载失败',
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[600],
),
),
if (!widget.isSentByMe && _hasError)
Padding(
padding: EdgeInsets.only(top: 8.h),
child: ElevatedButton(
onPressed: _retryDownload,
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h),
backgroundColor: Color(0xff8E7BF6),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.w),
),
),
child: Text(
'重新加载',
style: TextStyle(
fontSize: 10.sp,
color: Colors.white,
),
),
),
),
],
),
);
}
//
Widget _buildTimeLabel() {
return Container(
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: Text(
widget.formattedTime,
style: TextStyle(fontSize: 12.sp, color: Colors.grey),
),
),
);
}
//
Widget _buildAvatar() {
return Container(
width: 40.w,
height: 40.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.w),
image: DecorationImage(
image: AssetImage(Assets.imagesAvatarsExample),
fit: BoxFit.cover,
),
),
);
}
//
//
Widget _buildMessageStatus() {
//
if (!isSentByMe) {
return SizedBox.shrink();
}
if (!widget.isSentByMe) return SizedBox.shrink();
//
final status = message.status;
final status = widget.message.status;
if (status == MessageStatus.FAIL) {
//
return GestureDetector(
onTap: onResend,
onTap: widget.onResend,
child: Container(
width: 44.w,
height: 26.h,
@ -216,56 +620,56 @@ class ImageItem extends StatelessWidget {
),
);
} else if (status == MessageStatus.PROGRESS) {
//
// PROGRESS状态loading
if (_shouldHideProgressStatus) {
return SizedBox.shrink();
}
return Container(
width: 16.w,
height: 16.w,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.grey,
),
valueColor: AlwaysStoppedAnimation<Color>(Colors.grey),
),
);
} else {
//
return SizedBox.shrink();
}
}
//
Widget _buildErrorContainer(double width, double height) {
return Container(
width: width,
height: height,
padding: EdgeInsets.all(8.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18.w),
color: Colors.grey[200],
),
alignment: Alignment.center,
child: Icon(
Icons.image_not_supported,
size: 32.w,
color: Colors.grey[400],
),
);
}
//
void _onImageTap() {
// 使使
String? imagePath = imageBody.localPath;
String? imageUrl = imageBody.remotePath;
//
Get.to(
() => ImageViewerPage(
imagePath: imagePath,
imageUrl: imageUrl,
),
transition: Transition.fade,
duration: const Duration(milliseconds: 300),
);
//
bool hasImage = false;
String? imagePath;
String? imageUrl;
// 使
if (_currentLocalPath != null && _currentLocalPath!.isNotEmpty) {
final file = File(_currentLocalPath!);
if (file.existsSync()) {
imagePath = _currentLocalPath;
hasImage = true;
}
}
// 使
if (!hasImage && _currentRemotePath != null && _currentRemotePath!.isNotEmpty) {
imageUrl = _currentRemotePath;
hasImage = true;
}
if (hasImage) {
Get.to(
() => ImageViewerPage(
imagePath: imagePath,
imageUrl: imageUrl,
),
transition: Transition.fade,
duration: const Duration(milliseconds: 300),
);
} else {
SmartDialog.showToast('图片暂时无法查看');
}
}
}

66
lib/widget/message/more_options_view.dart

@ -1,10 +1,8 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
import 'package:wechat_camera_picker/wechat_camera_picker.dart';
import 'package:video_player/video_player.dart';
import '../../generated/assets.dart';
@ -12,13 +10,11 @@ class MoreOptionsView extends StatelessWidget {
final bool isVisible;
final ValueChanged<List<String>> onImageSelected;
final ValueChanged<String> onCameraSelected;
final Function(String filePath, int duration)? onVideoSelected;
const MoreOptionsView({
required this.isVisible,
required this.onImageSelected,
required this.onCameraSelected,
this.onVideoSelected,
super.key,
});
@ -42,38 +38,26 @@ class MoreOptionsView extends StatelessWidget {
GestureDetector(
onTap: () async{
try {
print('📷 [MoreOptionsView] 打开相册选择图片/视频');
//
print('📷 [MoreOptionsView] 打开相册选择图片');
//
List<AssetEntity>? result = await AssetPicker.pickAssets(
context,
pickerConfig: const AssetPickerConfig(
requestType: RequestType.common, //
requestType: RequestType.image, //
maxAssets: 9,
),
);
if (result != null && result.isNotEmpty) {
print('选择了 ${result.length} 个文件');
print('选择了 ${result.length} 张图片');
//
List<String> imagePaths = [];
for (var asset in result) {
final file = await asset.file;
if (file != null) {
print('文件类型: ${asset.type}, 路径: ${file.path}');
if (asset.type == AssetType.video) {
//
print('检测到视频文件');
final duration = asset.duration;
if (onVideoSelected != null) {
onVideoSelected!(file.path, duration);
}
} else {
//
imagePaths.add(file.path);
}
print('图片路径: ${file.path}');
imagePaths.add(file.path);
}
}
@ -84,7 +68,7 @@ class MoreOptionsView extends StatelessWidget {
}
}
} catch (e) {
print('❌ 选择文件失败: $e');
print('❌ 选择图片失败: $e');
if (Get.isLogEnable) {
Get.log("选择图片失败: $e");
}
@ -114,36 +98,24 @@ class MoreOptionsView extends StatelessWidget {
),
),
SizedBox(width: 40.w),
//
//
GestureDetector(
onTap: () async{
try {
print('📷 [MoreOptionsView] 打开相机');
//
//
AssetEntity? entity = await CameraPicker.pickFromCamera(
context,
pickerConfig: const CameraPickerConfig(
enableRecording: true, //
enableRecording: false, //
),
);
if (entity != null) {
final file = await entity.file;
if (file != null) {
print('文件类型: ${entity.type}, 路径: ${file.path}');
if (entity.type == AssetType.video) {
//
print('检测到视频文件');
final duration = await _getVideoDuration(file.path);
if (onVideoSelected != null) {
onVideoSelected!(file.path, duration);
}
} else {
//
print('检测到图片文件');
onCameraSelected(file.path);
}
print('拍摄的图片路径: ${file.path}');
onCameraSelected(file.path);
}
}
} catch (e) {
@ -184,18 +156,4 @@ class MoreOptionsView extends StatelessWidget {
: null,
);
}
//
Future<int> _getVideoDuration(String filePath) async {
try {
final controller = VideoPlayerController.file(File(filePath));
await controller.initialize();
final duration = controller.value.duration.inSeconds;
await controller.dispose();
return duration;
} catch (e) {
print('获取视频时长失败: $e');
return 0;
}
}
}

119
lib/widget/message/voice_item.dart

@ -101,41 +101,128 @@ class _VoiceItemState extends State<VoiceItem> with TickerProviderStateMixin {
try {
//
String? filePath;
final localPath = widget.voiceBody.localPath;
var localPath = widget.voiceBody.localPath;
final remotePath = widget.voiceBody.remotePath;
// 使
if (remotePath != null && remotePath.isNotEmpty) {
// 使audioplayers URL
// URL
if (remotePath.startsWith('http://') ||
remotePath.startsWith('https://')) {
filePath = remotePath;
print('🔍 语音消息路径检查: localPath=$localPath, remotePath=$remotePath');
// 0
if (!widget.isSentByMe && widget.message != null) {
final localFile = localPath.isNotEmpty ? File(localPath) : null;
bool needDownload = false;
if (localPath.isEmpty || localFile == null || !await localFile.exists()) {
needDownload = true;
print('📥 本地文件不存在,需要下载');
} else {
SmartDialog.showToast('音频文件不存在,请等待下载完成');
print('远程音频文件路径: $remotePath');
return;
final fileSize = await localFile.length();
if (fileSize == 0) {
needDownload = true;
print('📥 本地文件大小为0,需要下载');
}
}
//
if (needDownload) {
try {
print('📥 开始下载语音文件...');
await EMClient.getInstance.chatManager
.downloadAttachment(widget.message!);
//
await Future.delayed(Duration(milliseconds: 300));
// body
// voiceBody
if (widget.message!.body is EMVoiceMessageBody) {
final updatedVoiceBody = widget.message!.body as EMVoiceMessageBody;
localPath = updatedVoiceBody.localPath;
print('✅ 语音文件下载完成,新路径: $localPath');
} else {
print('⚠️ 消息 body 类型不是 EMVoiceMessageBody');
}
//
setState(() {});
} catch (e) {
print('❌ 下载语音文件失败: $e');
SmartDialog.showToast('下载语音文件失败: $e');
return;
}
}
} else if (localPath.isNotEmpty) {
}
// 使
if (localPath.isNotEmpty) {
final localFile = File(localPath);
if (await localFile.exists()) {
filePath = localPath;
//
final fileSize = await localFile.length();
if (fileSize > 0) {
filePath = localPath;
print('✅ 使用本地音频文件: $localPath, 文件大小: $fileSize bytes');
} else {
print('⚠️ 本地音频文件大小为0: $localPath');
// 0使
if (remotePath != null && remotePath.isNotEmpty) {
if (remotePath.startsWith('http://') ||
remotePath.startsWith('https://')) {
filePath = remotePath;
print('✅ 使用远程音频文件: $remotePath');
} else {
SmartDialog.showToast('音频文件无效');
print('⚠️ 本地文件大小为0,远程路径不是URL: $remotePath');
return;
}
} else {
SmartDialog.showToast('音频文件无效');
print('⚠️ 本地文件大小为0,且没有远程路径');
return;
}
}
} else {
print('⚠️ 本地音频文件不存在: $localPath');
// 使
if (remotePath != null && remotePath.isNotEmpty) {
if (remotePath.startsWith('http://') ||
remotePath.startsWith('https://')) {
filePath = remotePath;
print('✅ 使用远程音频文件: $remotePath');
} else {
SmartDialog.showToast('音频文件不存在');
print('⚠️ 本地文件不存在,远程路径不是URL: $remotePath');
return;
}
} else {
SmartDialog.showToast('音频文件不存在');
print('⚠️ 本地和远程路径都无效');
return;
}
}
} else if (remotePath != null && remotePath.isNotEmpty) {
// 使
if (remotePath.startsWith('http://') ||
remotePath.startsWith('https://')) {
// HTTP URL
filePath = remotePath;
print('✅ 使用远程音频文件(无本地路径): $remotePath');
} else {
SmartDialog.showToast('音频文件不存在');
print('本地音频文件不存在: $localPath');
print('⚠️ 远程路径不是URL: $remotePath');
return;
}
}
if (filePath != null && filePath.isNotEmpty) {
print('🎵 开始播放音频: $filePath');
await _playerManager.play(widget.messageId, filePath);
} else {
SmartDialog.showToast('无法获取音频文件');
print('音频文件路径为空');
print('音频文件路径为空');
}
} catch (e) {
SmartDialog.showToast('播放失败: $e');
print('播放音频失败: $e');
print('播放音频失败: $e');
}
}

Loading…
Cancel
Save