diff --git a/assets/images/video_call.png b/assets/images/video_call.png new file mode 100644 index 0000000..f991d8d Binary files /dev/null and b/assets/images/video_call.png differ diff --git a/lib/controller/message/conversation_controller.dart b/lib/controller/message/conversation_controller.dart index edee404..34a6c3d 100644 --- a/lib/controller/message/conversation_controller.dart +++ b/lib/controller/message/conversation_controller.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'package:get/get.dart'; import 'package:im_flutter_sdk/im_flutter_sdk.dart'; import '../../im/im_manager.dart'; @@ -389,7 +390,28 @@ class ConversationController extends GetxController { if(message.body.type == MessageType.TXT){ final body = message.body as EMTextMessageBody; - return body.content; + final content = body.content; + + // 检查是否是CALL消息 + if (content != null && content.startsWith('[CALL:]')) { + try { + final jsonStr = content.substring(7); // 移除 '[CALL:]' 前缀 + final callInfo = jsonDecode(jsonStr) as Map; + final callType = callInfo['callType'] as String?; + if (callType == 'video') { + return '[视频通话]'; + } else if (callType == 'voice') { + return '[语音通话]'; + } + } catch (e) { + // 解析失败,返回原始内容 + if (Get.isLogEnable) { + Get.log('⚠️ [ConversationController] 解析CALL消息失败: $e'); + } + } + } + + return content ?? ''; }else if(message.body.type == MessageType.IMAGE){ return '[图片]'; }else if(message.body.type == MessageType.VOICE){ diff --git a/lib/generated/assets.dart b/lib/generated/assets.dart index ee639aa..50c6531 100644 --- a/lib/generated/assets.dart +++ b/lib/generated/assets.dart @@ -124,6 +124,7 @@ class Assets { static const String imagesHiIcon = 'assets/images/hi_icon.png'; static const String imagesHomeNol = 'assets/images/home_nol.png'; static const String imagesHomePre = 'assets/images/home_pre.png'; + static const String imagesImCoinIcon = 'assets/images/im_coin_icon.png'; static const String imagesInformationBg = 'assets/images/information_bg.png'; static const String imagesLastMsgIcon = 'assets/images/last_msg_icon.png'; static const String imagesLimitTime = 'assets/images/limit_time.png'; @@ -201,6 +202,7 @@ class Assets { static const String imagesUserAvatar = 'assets/images/user_avatar.png'; static const String imagesVerifiedIcon = 'assets/images/verified_icon.png'; static const String imagesVideo = 'assets/images/video.png'; + static const String imagesVideoCall = 'assets/images/video_call.png'; static const String imagesVip = 'assets/images/vip.png'; static const String imagesVipBanner = 'assets/images/vip_banner.png'; static const String imagesVipBg = 'assets/images/vip_bg.png'; @@ -221,5 +223,5 @@ class Assets { static const String imagesWallet = 'assets/images/wallet.png'; static const String imagesWechatPay = 'assets/images/wechat_pay.png'; static const String imagesWomenIcon = 'assets/images/women_icon.png'; - static const String imagesImCoinIcon = 'assets/images/im_coin_icon.png'; + } diff --git a/lib/im/im_manager.dart b/lib/im/im_manager.dart index 911c089..b2b56ed 100644 --- a/lib/im/im_manager.dart +++ b/lib/im/im_manager.dart @@ -1,5 +1,6 @@ import 'dart:io'; import 'dart:async'; +import 'dart:convert'; import 'package:flutter/widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -1547,7 +1548,28 @@ class IMManager { try { if (message.body.type == MessageType.TXT) { final body = message.body as EMTextMessageBody; - return body.content ?? ''; + final content = body.content; + + // 检查是否是CALL消息 + if (content != null && content.startsWith('[CALL:]')) { + try { + final jsonStr = content.substring(7); // 移除 '[CALL:]' 前缀 + final callInfo = jsonDecode(jsonStr) as Map; + final callType = callInfo['callType'] as String?; + if (callType == 'video') { + return '[视频通话]'; + } else if (callType == 'voice') { + return '[语音通话]'; + } + } catch (e) { + // 解析失败,返回原始内容 + if (Get.isLogEnable) { + Get.log('⚠️ [IMManager] 解析CALL消息失败: $e'); + } + } + } + + return content ?? ''; } else if (message.body.type == MessageType.IMAGE) { return '[图片]'; } else if (message.body.type == MessageType.VOICE) { diff --git a/lib/pages/message/chat_page.dart b/lib/pages/message/chat_page.dart index 3e8629d..b2e6bf9 100644 --- a/lib/pages/message/chat_page.dart +++ b/lib/pages/message/chat_page.dart @@ -4,6 +4,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:im_flutter_sdk/im_flutter_sdk.dart'; +import '../../controller/message/call_manager.dart'; import '../../controller/message/chat_controller.dart'; import '../../controller/message/voice_player_manager.dart'; // import '../../controller/message/call_manager.dart'; // 暂时隐藏 @@ -14,6 +15,7 @@ import '../../../widget/message/message_item.dart'; import '../../../widget/message/chat_gift_popup.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'chat_settings_page.dart'; +import 'video_call_page.dart'; import '../home/user_information_page.dart'; import '../../../widget/live/live_recharge_popup.dart'; @@ -298,15 +300,21 @@ class _ChatPageState extends State { // chatController: controller, // ); // }, - // 视频通话回调(暂时隐藏) - // onVideoCall: () async { - // // 发起视频通话 - // await CallManager.instance.initiateCall( - // targetUserId: widget.userId, - // callType: CallType.video, - // chatController: controller, - // ); - // }, + // 视频通话回调 + onVideoCall: () async { + // 发起视频通话并跳转到视频通话页面 + await CallManager.instance.initiateCall( + targetUserId: widget.userId, + callType: CallType.video, + chatController: controller, + ); + // 跳转到视频通话页面 + Get.to(() => VideoCallPage( + targetUserId: widget.userId, + userData: widget.userData ?? controller.userData, + isInitiator: true, + )); + }, ), ], ), diff --git a/lib/pages/message/video_call_page.dart b/lib/pages/message/video_call_page.dart new file mode 100644 index 0000000..861a2e5 --- /dev/null +++ b/lib/pages/message/video_call_page.dart @@ -0,0 +1,427 @@ +import 'dart:async'; +import 'dart:ui'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:dating_touchme_app/generated/assets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import '../../controller/message/call_manager.dart'; +import '../../controller/message/conversation_controller.dart'; +import '../../model/home/marriage_data.dart'; + +/// 视频通话页面 +class VideoCallPage extends StatefulWidget { + final String targetUserId; + final MarriageData? userData; + final bool isInitiator; // 是否是发起方 + + const VideoCallPage({ + super.key, + required this.targetUserId, + this.userData, + this.isInitiator = true, + }); + + @override + State createState() => _VideoCallPageState(); +} + +class _VideoCallPageState extends State { + final CallManager _callManager = CallManager.instance; + + bool _isMicMuted = false; + bool _isSpeakerOn = false; + Duration _callDuration = Duration.zero; + Timer? _durationTimer; + + String? _targetUserName; + String? _targetAvatarUrl; + + // 通话是否已接通 + bool _isCallConnected = false; + + @override + void initState() { + super.initState(); + _initializeCall(); + _loadUserInfo(); + _initCallStatus(); + _startDurationTimer(); + + // 设置系统UI样式 + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + ]); + } + + /// 初始化通话状态 + void _initCallStatus() { + // 检查当前通话状态 + final callSession = _callManager.currentCall.value; + if (callSession != null && _callManager.callDurationSeconds.value > 0) { + // 如果通话已存在且已经开始计时,说明已接通 + _isCallConnected = true; + _callDuration = Duration(seconds: _callManager.callDurationSeconds.value); + } else { + // 否则是未接通状态 + _isCallConnected = false; + } + } + + /// 加载用户信息 + Future _loadUserInfo() async { + // 优先使用传入的 userData + if (widget.userData != null) { + setState(() { + _targetUserName = widget.userData!.nickName.isNotEmpty + ? widget.userData!.nickName + : widget.targetUserId; + _targetAvatarUrl = widget.userData!.profilePhoto; + }); + return; + } + + // 如果没有传入 userData,尝试从 ConversationController 获取 + try { + if (Get.isRegistered()) { + final conversationController = Get.find(); + + // 先从缓存中获取 + final cachedUserInfo = conversationController.getCachedUserInfo(widget.targetUserId); + if (cachedUserInfo != null && (cachedUserInfo.nickName != null || cachedUserInfo.avatarUrl != null)) { + setState(() { + _targetUserName = cachedUserInfo.nickName ?? widget.targetUserId; + _targetAvatarUrl = cachedUserInfo.avatarUrl; + }); + return; + } + + // 如果缓存中没有,尝试从 IM 加载 + final userInfo = await conversationController.loadContact(widget.targetUserId); + if (userInfo != null && (userInfo.nickName != null || userInfo.avatarUrl != null)) { + setState(() { + _targetUserName = userInfo.nickName ?? widget.targetUserId; + _targetAvatarUrl = userInfo.avatarUrl; + }); + return; + } + } + } catch (e) { + print('⚠️ [VideoCallPage] 加载用户信息失败: $e'); + } + + // 如果都获取不到,使用默认值 + setState(() { + _targetUserName = widget.targetUserId; + _targetAvatarUrl = null; + }); + } + + @override + void dispose() { + _durationTimer?.cancel(); + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); + SystemChrome.setPreferredOrientations(DeviceOrientation.values); + super.dispose(); + } + + /// 初始化通话 + Future _initializeCall() async { + try { + // TODO: 初始化RTC Engine并加入频道 + // await _rtcManager.initialize(appId: 'your_app_id'); + // await _rtcManager.joinChannel(token: 'token', channelId: 'channel_id', uid: uid); + } catch (e) { + print('初始化通话失败: $e'); + } + } + + /// 开始通话时长计时器 + void _startDurationTimer() { + // 监听 CallManager 的通话状态变化 + _callManager.currentCall.listen((callSession) { + if (mounted) { + final wasConnected = _isCallConnected; + // 如果通话存在且已经开始计时,说明已接通 + if (callSession != null && _callManager.callDurationSeconds.value > 0) { + _isCallConnected = true; + if (!wasConnected) { + // 刚接通,同步时长 + _callDuration = Duration(seconds: _callManager.callDurationSeconds.value); + } + } else if (callSession == null) { + _isCallConnected = false; + } + setState(() {}); + } + }); + + // 监听通话时长变化(已接通时更新) + _callManager.callDurationSeconds.listen((seconds) { + if (mounted && _isCallConnected) { + setState(() { + _callDuration = Duration(seconds: seconds); + }); + } else if (mounted && !_isCallConnected && seconds > 0) { + // 如果时长开始增加,说明刚接通 + setState(() { + _isCallConnected = true; + _callDuration = Duration(seconds: seconds); + }); + } + }); + + // 如果未接通,使用本地计时器检查状态变化 + if (!_isCallConnected) { + _durationTimer = Timer.periodic(Duration(seconds: 1), (timer) { + if (mounted) { + final callSession = _callManager.currentCall.value; + final duration = _callManager.callDurationSeconds.value; + + // 检查是否已接通(通话存在且时长大于0) + if (callSession != null && duration > 0) { + _isCallConnected = true; + _callDuration = Duration(seconds: duration); + timer.cancel(); + setState(() {}); + } + } + }); + } + } + + /// 格式化通话时长 + String _formatDuration(Duration duration) { + String twoDigits(int n) => n.toString().padLeft(2, '0'); + final minutes = twoDigits(duration.inMinutes.remainder(60)); + final seconds = twoDigits(duration.inSeconds.remainder(60)); + return '$minutes:$seconds'; + } + + /// 切换麦克风状态 + void _toggleMic() { + setState(() { + _isMicMuted = !_isMicMuted; + }); + // TODO: 调用RTC Manager切换麦克风 + // _rtcManager.enableAudio(!_isMicMuted); + } + + /// 切换扬声器状态 + void _toggleSpeaker() { + setState(() { + _isSpeakerOn = !_isSpeakerOn; + }); + // TODO: 调用RTC Manager切换扬声器 + // _rtcManager.setEnableSpeakerphone(_isSpeakerOn); + } + + /// 挂断通话 + void _hangUp() async { + try { + // TODO: 离开RTC频道 + // await _rtcManager.leaveChannel(); + + // 结束通话(传递通话时长) + await _callManager.endCall(callDuration: _callDuration.inSeconds); + + // 返回上一页 + Get.back(); + } catch (e) { + print('挂断通话失败: $e'); + Get.back(); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: Stack( + children: [ + // 背景视频/头像(模糊) + _buildBackground(), + + // 用户信息 + _buildUserInfo(), + + // 通话时长 + _buildCallDuration(), + + // 底部控制按钮 + _buildControlButtons(), + ], + ), + ); + } + + /// 构建背景 + Widget _buildBackground() { + return SizedBox( + width: double.infinity, + height: 1.sh, + child: _targetAvatarUrl != null && _targetAvatarUrl!.isNotEmpty + ? ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: CachedNetworkImage( + imageUrl: _targetAvatarUrl!, + fit: BoxFit.cover, + width: double.infinity, + height: double.infinity, + errorWidget: (context, url, error) => _buildDefaultBackground(), + ), + ) + : _buildDefaultBackground(), + ); + } + + /// 构建默认背景 + Widget _buildDefaultBackground() { + return Container( + color: Colors.black, + ); + } + + /// 构建用户信息 + Widget _buildUserInfo() { + return Positioned( + top: MediaQuery.of(context).size.height * 0.15, + left: 0, + right: 0, + child: Column( + children: [ + // 头像 + ClipOval( + child: _targetAvatarUrl != null && _targetAvatarUrl!.isNotEmpty + ? CachedNetworkImage( + imageUrl: _targetAvatarUrl!, + width: 120.w, + height: 120.w, + fit: BoxFit.cover, + errorWidget: (context, url, error) => Image.asset( + Assets.imagesUserAvatar, + width: 120.w, + height: 120.w, + fit: BoxFit.cover, + ), + ) + : Image.asset( + Assets.imagesUserAvatar, + width: 120.w, + height: 120.w, + fit: BoxFit.cover, + ), + ), + SizedBox(height: 16.h), + // 用户名 + Text( + _targetUserName ?? widget.targetUserId, + style: TextStyle( + color: Colors.white, + fontSize: 24.sp, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } + + /// 构建通话时长 + Widget _buildCallDuration() { + return Positioned( + bottom: MediaQuery.of(context).size.height * 0.25, + left: 0, + right: 0, + child: Center( + child: Text( + _isCallConnected ? _formatDuration(_callDuration) : '正在呼叫中', + style: TextStyle( + color: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + ); + } + + /// 构建控制按钮 + Widget _buildControlButtons() { + return Positioned( + bottom: 40.h, + left: 0, + right: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // 免提按钮 + _buildControlButton( + icon: Icons.volume_up, + label: '免提', + isActive: _isSpeakerOn, + onTap: _toggleSpeaker, + ), + // 麦克风按钮 + _buildControlButton( + icon: Icons.mic, + label: '麦克风', + isActive: !_isMicMuted, + onTap: _toggleMic, + ), + // 挂断按钮 + _buildControlButton( + icon: Icons.call_end, + label: '挂断', + isActive: true, + onTap: _hangUp, + isHangUp: true, + ), + ], + ), + ); + } + + /// 构建控制按钮 + Widget _buildControlButton({ + required IconData icon, + required String label, + required bool isActive, + required VoidCallback onTap, + bool isHangUp = false, + }) { + return GestureDetector( + onTap: onTap, + child: Column( + children: [ + Container( + width: 56.w, + height: 56.w, + decoration: BoxDecoration( + color: isHangUp + ? Color(0xFFFF3B30) + : (isActive ? Colors.white.withOpacity(0.3) : Colors.white.withOpacity(0.2)), + shape: BoxShape.circle, + ), + child: Icon( + icon, + color: Colors.white, + size: 28.w, + ), + ), + SizedBox(height: 8.h), + Text( + label, + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + ), + ), + ], + ), + ); + } +} + diff --git a/lib/widget/message/chat_input_bar.dart b/lib/widget/message/chat_input_bar.dart index d138328..7ab847a 100644 --- a/lib/widget/message/chat_input_bar.dart +++ b/lib/widget/message/chat_input_bar.dart @@ -300,13 +300,13 @@ class _ChatInputBarState extends State { // widget.onVoiceCall?.call(); // }), // 视频通话按钮(暂时隐藏) - // Image.asset( - // Assets.imagesSendVideoCall, - // width: 24.w, - // height: 24.w, - // ).onTap(() { - // widget.onVideoCall?.call(); - // }), + Image.asset( + Assets.imagesVideoCall, + width: 24.w, + height: 24.w, + ).onTap(() { + widget.onVideoCall?.call(); + }), // 礼物按钮 Image.asset(Assets.imagesGift, width: 24.w, height: 24.w).onTap(() { widget.onGiftTap?.call();