diff --git a/.gitignore b/.gitignore index e469c2b..2ea8576 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ .packages build/ # If you're building an application, you may want to check-in your pubspec.lock -pubspec.lock +/pubspec.lock # Directory created by dartdoc # If you don't generate documentation locally you can remove this line. diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 5d844db..77c1cb3 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -37,6 +37,15 @@ android { signingConfig = signingConfigs.getByName("debug") } } + + packaging { + jniLibs { + pickFirsts += listOf("lib/arm64-v8a/libaosl.so") + pickFirsts += listOf("lib/armeabi-v7a/libaosl.so") + pickFirsts += listOf("lib/x86/libaosl.so") + pickFirsts += listOf("lib/x86_64/libaosl.so") + } + } } flutter { diff --git a/lib/controller/global.dart b/lib/controller/global.dart new file mode 100644 index 0000000..84c3dbb --- /dev/null +++ b/lib/controller/global.dart @@ -0,0 +1,33 @@ +// ignore_for_file: constant_identifier_names, non_constant_identifier_names +import 'dart:io'; + +import '../model/mine/user_data.dart'; + +class GlobalData { + String? qnToken;//uec接口的Token + String? userId;//用户id + UserData? userData;// 用户的基础信息 + + bool isLogout = false;//是否已经退出登录 + + void logout() { + isLogout = true; + userId = null; + qnToken = null; + userData = null; + } + + static GlobalData getInstance() { + _instance ??= GlobalData._init(); + return _instance!; + } + + GlobalData._init() { + if(Platform.isIOS){ + // xAppId = "503258978847966412"; + } + } + factory GlobalData() => getInstance(); + static GlobalData get instance => getInstance(); + static GlobalData? _instance; +} \ No newline at end of file diff --git a/lib/controller/message/chat_controller.dart b/lib/controller/message/chat_controller.dart index bf749a5..c98df36 100644 --- a/lib/controller/message/chat_controller.dart +++ b/lib/controller/message/chat_controller.dart @@ -21,11 +21,20 @@ class ChatController extends GetxController { @override void onInit() { super.onInit(); + // 注册到 IMManager,以便接收消息时能通知到此 Controller + IMManager.instance.registerChatController(this); // 初始化时获取用户信息和消息列表 fetchUserInfo(); fetchMessages(); } + @override + void onClose() { + // 注销 ChatController + IMManager.instance.unregisterChatController(userId); + super.onClose(); + } + /// 获取用户信息 Future fetchUserInfo() async { try { @@ -55,7 +64,7 @@ class ChatController extends GetxController { // 发送成功后将消息添加到列表开头 messages.insert(0, message); update(); - // 刷新会话列表 + // 更新会话列表 _refreshConversationList(); return true; } @@ -79,7 +88,7 @@ class ChatController extends GetxController { // 发送成功后将消息添加到列表开头 messages.insert(0, message); update(); - // 刷新会话列表 + // 更新会话列表 _refreshConversationList(); return true; } @@ -104,7 +113,7 @@ class ChatController extends GetxController { // 发送成功后将消息添加到列表开头 messages.insert(0, message); update(); - // 刷新会话列表 + // 更新会话列表 _refreshConversationList(); return true; } @@ -123,7 +132,7 @@ class ChatController extends GetxController { print('🎬 [ChatController] 准备发送视频消息'); print('视频路径: $filePath'); print('视频时长: $duration 秒'); - + final message = await IMManager.instance.sendVideoMessage( filePath, userId, @@ -135,7 +144,7 @@ class ChatController extends GetxController { // 发送成功后将消息添加到列表开头 messages.insert(0, message); update(); - // 刷新会话列表 + // 更新会话列表 _refreshConversationList(); return true; } @@ -198,14 +207,32 @@ class ChatController extends GetxController { } } + /// 添加接收到的消息 + void addReceivedMessage(EMMessage message) { + // 检查消息是否已存在(避免重复添加) + if (!messages.any((msg) => msg.msgId == message.msgId)) { + // 将新消息添加到列表开头 + messages.insert(0, message); + update(); + // 更新会话列表 + _refreshConversationList(); + + if (Get.isLogEnable) { + Get.log('收到新消息并添加到列表: ${message.msgId}'); + } + } + } + /// 刷新会话列表 void _refreshConversationList() { try { - // 获取会话控制器实例并刷新 + // 尝试获取 ConversationController 并刷新会话列表 if (Get.isRegistered()) { - Get.find().refreshConversations(); + final conversationController = Get.find(); + conversationController.refreshConversations(); } } catch (e) { + // ConversationController 可能未注册,忽略错误 if (Get.isLogEnable) { Get.log('刷新会话列表失败: $e'); } diff --git a/lib/controller/message/voice_player_manager.dart b/lib/controller/message/voice_player_manager.dart index 0f498c6..af94c32 100644 --- a/lib/controller/message/voice_player_manager.dart +++ b/lib/controller/message/voice_player_manager.dart @@ -49,7 +49,11 @@ class VoicePlayerManager extends GetxController { // 播放新音频 _currentPlayingId = audioId; currentPlayingId.value = audioId; - await _audioPlayer.play(DeviceFileSource(filePath)); + if(filePath.startsWith('https://')){ + await _audioPlayer.play(UrlSource(filePath)); + }else{ + await _audioPlayer.play(DeviceFileSource(filePath)); + } } catch (e) { print('播放音频失败: $e'); _currentPlayingId = null; diff --git a/lib/controller/mine/auth_controller.dart b/lib/controller/mine/auth_controller.dart new file mode 100644 index 0000000..9c7dfa8 --- /dev/null +++ b/lib/controller/mine/auth_controller.dart @@ -0,0 +1,131 @@ +import 'dart:async'; +import 'package:get/get.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import '../../network/user_api.dart'; +import '../global.dart'; + +class AuthController extends GetxController { + final isLoading = false.obs; + final List dataList = []; + // 是否正在登录中 + final isLoggingIn = false.obs; + final name = ''.obs; + final idcard = ''.obs; + final agree = false.obs; + // 从GetX依赖注入中获取UserApi实例 + late UserApi _userApi; + @override + void onInit() { + super.onInit(); + // 从全局依赖中获取UserApi + _userApi = Get.find(); + _loadInitialData(); + } + + // 登录方法 + Future _loadInitialData() async { + try { + isLoading.value = true; + late bool realAuth = false; + if(GlobalData().userData?.realAuth != null){ + realAuth = GlobalData().userData!.realAuth!; + } + dataList.assignAll([ + AuthCard( title: '手机绑定', desc: '防止账号丢失', index: 1, authed: true), + AuthCard( title: '真实头像', desc: '提高交友成功率', index: 2, authed: false), + AuthCard( title: '实名认证', desc: '提高交友成功率', index: 3, authed: false), + ]); + // 调用登录接口 + // final response = await _userApi.login({}); + // 处理响应 + // if (response.data.isSuccess) { + // + // } + } catch (e) { + SmartDialog.showToast('网络请求失败,请检查网络连接'); + } finally { + isLoading.value = false; + } + } + + bool validateChineseID(String id) { + if (id.length != 18) return false; + + // 系数表 + final coefficients = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; + + // 校验码对应表 + final checkCodeMap = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']; + + int sum = 0; + + try { + for (int i = 0; i < 17; i++) { + int digit = int.parse(id[i]); + sum += digit * coefficients[i]; + } + } catch (e) { + return false; // 包含非数字字符 + } + + int remainder = sum % 11; + String checkCode = checkCodeMap[remainder]; + + return id[17].toUpperCase() == checkCode; + } + + Future startAuthing() async { + if (name.value.isEmpty) { + SmartDialog.showToast('请输入姓名'); + return; + } + if (idcard.value.isEmpty) { + SmartDialog.showToast('请输入身份证号'); + return; + } + if (!validateChineseID(idcard.value)) { + SmartDialog.showToast('请输入正确的身份证号'); + return; + } + if (!agree.value) { + SmartDialog.showToast('请同意用户认证协议'); + return; + } + try { + // 调用登录接口 + final param = { + 'miId': GlobalData().userData?.id, + 'authenticationCode': 0, + 'value': '${name.value},${idcard.value}', + }; + final response = await _userApi.saveCertificationAudit(param); + // 处理响应 + if (response.data.isSuccess) { + GlobalData().userData?.realAuth = true; + SmartDialog.showToast('认证成功'); + Get.back(result: {'index': 3}); + } else { + SmartDialog.showToast(response.data.message); + } + } catch (e) { + SmartDialog.showToast('网络请求失败,请检查网络连接'); + } finally { + + } + } + +} + +class AuthCard { + final String title; + final String desc; + final int index; + final bool authed; + + AuthCard({ + required this.desc, + required this.title, + required this.index, + required this.authed, + }); +} diff --git a/lib/controller/mine/mine_controller.dart b/lib/controller/mine/mine_controller.dart index b32c408..29e7bf8 100644 --- a/lib/controller/mine/mine_controller.dart +++ b/lib/controller/mine/mine_controller.dart @@ -1,4 +1,5 @@ import 'package:dating_touchme_app/generated/assets.dart'; +import 'package:dating_touchme_app/pages/mine/auth_center_page.dart'; import 'package:dating_touchme_app/pages/mine/my_wallet_page.dart'; import 'package:dating_touchme_app/pages/mine/rose_page.dart'; import 'package:get/get.dart'; @@ -18,7 +19,7 @@ class MineController extends GetxController { {"icon": Assets.imagesRose, "title": "我的玫瑰", "subTitle": "新人限时福利", "path": () => RosePage()}, {"icon": Assets.imagesWallet, "title": "我的钱包", "subTitle": "提现无门槛", "path": () => MyWalletPage()}, {"icon": Assets.imagesShop, "title": "商城中心", "subTitle": "不定期更新商品", "path": () => Null}, - {"icon": Assets.imagesCert, "title": "认证中心", "subTitle": "未认证", "path": () => Null}, + {"icon": Assets.imagesCert, "title": "认证中心", "subTitle": "未认证", "path": () => AuthCenterPage()}, ].obs; List settingList = [ diff --git a/lib/controller/mine/user_controller.dart b/lib/controller/mine/user_controller.dart index 02929d1..045fe50 100644 --- a/lib/controller/mine/user_controller.dart +++ b/lib/controller/mine/user_controller.dart @@ -2,9 +2,12 @@ import 'package:dating_touchme_app/im/im_manager.dart'; import 'package:dating_touchme_app/oss/oss_manager.dart'; import 'package:get/get.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import '../../model/mine/authentication_data.dart'; +import '../../model/mine/user_data.dart'; import '../../network/user_api.dart'; import '../../pages/mine/user_info_page.dart'; import '../../pages/main/main_page.dart'; +import '../global.dart'; class UserController extends GetxController { @@ -72,8 +75,15 @@ class UserController extends GetxController { final response = await _userApi.getMarriageInformationDetail(); if (response.data.isSuccess) { // 检查data是否为null或者是空对象 - final information = response.data.data; - if (information == null || information.id.isEmpty || information.genderCode.isNaN || information.birthYear == null) { + final information = response.data.data!; + if (information.id.isNotEmpty) { + final result = await _userApi.getCertificationList(information.id); + List list = result.data.data!; + final record = list.firstWhere((item) => item.authenticationCode == 0); + information.realAuth = record.status == 1; + } + GlobalData().userData = information; + if (information.id.isEmpty || information.genderCode.isNaN || information.birthYear == null) { //跳转到完善信息 SmartDialog.showToast('转到完善信息'); // 导航到完善信息页面 diff --git a/lib/im/im_manager.dart b/lib/im/im_manager.dart index 456e5d5..78ff82c 100644 --- a/lib/im/im_manager.dart +++ b/lib/im/im_manager.dart @@ -1,5 +1,8 @@ +import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:im_flutter_sdk/im_flutter_sdk.dart'; +import '../controller/message/conversation_controller.dart'; +import '../controller/message/chat_controller.dart'; // 完整的IM管理器实现,使用实际的SDK类型和方法 class IMManager { @@ -12,6 +15,9 @@ class IMManager { bool _isInitialized = false; + // 存储活跃的 ChatController 实例,key 为 userId + final Map _activeChatControllers = {}; + IMManager._internal() { print('IMManager instance created'); } @@ -71,6 +77,12 @@ class IMManager { "", EMChatEventHandler( onMessagesReceived: (messages) { + // 收到新消息时,更新会话列表 + _refreshConversationList(); + + // 通知对应的 ChatController 更新消息列表 + _notifyChatControllers(messages); + for (var msg in messages) { switch (msg.body.type) { case MessageType.TXT: @@ -167,16 +179,16 @@ class IMManager { /// 发送语音消息 Future sendVoiceMessage( - String filePath, - String toChatUsername, - int duration - ) async { + String filePath, + String toChatUsername, + int duration, + ) async { try { // 创建图片消息 final message = EMMessage.createVoiceSendMessage( targetId: toChatUsername, filePath: filePath, - duration: duration + duration: duration, ); // 发送消息 @@ -223,14 +235,14 @@ class IMManager { print('视频路径: $videoPath'); print('接收用户: $toChatUsername'); print('视频时长: $duration 秒'); - + // 创建视频消息 final message = EMMessage.createVideoSendMessage( targetId: toChatUsername, filePath: videoPath, duration: duration, ); - + print('消息创建成功,消息类型: ${message.body.type}'); print('消息体是否为视频: ${message.body is EMVideoMessageBody}'); @@ -280,6 +292,67 @@ class IMManager { return data[userId]; } + /// 注册 ChatController + void registerChatController(ChatController controller) { + _activeChatControllers[controller.userId] = controller; + if (Get.isLogEnable) { + Get.log('注册 ChatController: ${controller.userId}'); + } + } + + /// 注销 ChatController + void unregisterChatController(String userId) { + _activeChatControllers.remove(userId); + if (Get.isLogEnable) { + Get.log('注销 ChatController: $userId'); + } + } + + /// 通知 ChatController 更新消息列表 + void _notifyChatControllers(List messages) { + try { + // 遍历所有收到的消息 + for (var message in messages) { + // 只处理接收到的消息(direction == RECEIVE) + if (message.direction == MessageDirection.RECEIVE) { + // 获取消息的发送者ID(from 属性) + final fromId = message.from; + + if (fromId != null && fromId.isNotEmpty) { + // 查找对应的 ChatController + final controller = _activeChatControllers[fromId]; + if (controller != null) { + controller.addReceivedMessage(message); + if (Get.isLogEnable) { + Get.log('通知 ChatController 更新消息: $fromId'); + } + } + } + } + } + } catch (e) { + if (Get.isLogEnable) { + Get.log('通知 ChatController 更新消息列表失败: $e'); + } + } + } + + /// 刷新会话列表 + void _refreshConversationList() { + try { + // 尝试获取 ConversationController 并刷新会话列表 + if (Get.isRegistered()) { + final conversationController = Get.find(); + conversationController.refreshConversations(); + } + } catch (e) { + // ConversationController 可能未注册,忽略错误 + if (Get.isLogEnable) { + Get.log('刷新会话列表失败: $e'); + } + } + } + /// 清理资源 void dispose() { try { diff --git a/lib/main.dart b/lib/main.dart index b2d0f4d..265b61e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:dating_touchme_app/network/network_service.dart'; import 'package:dating_touchme_app/pages/main/main_page.dart'; import 'package:dating_touchme_app/pages/mine/login_page.dart'; import 'package:dating_touchme_app/pages/mine/user_info_page.dart'; +import 'package:dating_touchme_app/rtc/rtc_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -21,6 +22,7 @@ void main() async { // 设置环境配置 - 根据是否为release模式 EnvConfig.setEnvironment(Environment.dev); + RTCManager.instance.initialize(appId: '4c2ea9dcb4c5440593a418df0fdd512d'); IMManager.instance.initialize('1165251016193374#demo'); // 初始化全局依赖 final networkService = NetworkService(); diff --git a/lib/model/mine/authentication_data.dart b/lib/model/mine/authentication_data.dart new file mode 100644 index 0000000..952a35e --- /dev/null +++ b/lib/model/mine/authentication_data.dart @@ -0,0 +1,31 @@ +class AuthenticationData { + final int? authenticationCode; + final String? authenticationName; + final String? miId; + final int? status; + + AuthenticationData({ + this.authenticationCode, + this.authenticationName, + this.miId, + this.status, + }); + + factory AuthenticationData.fromJson(Map json) { + return AuthenticationData( + authenticationCode: json['authenticationCode'] as int?, + authenticationName: json['authenticationName'] as String?, + miId: json['miId'] as String?, + status: json['status'] as int?, + ); + } + + Map toJson() { + return { + 'authenticationCode': authenticationCode, + 'authenticationName': authenticationName, + 'miId': miId, + 'status': status, + }; + } +} \ No newline at end of file diff --git a/lib/model/mine/user_data.dart b/lib/model/mine/user_data.dart index fc2fa39..0b9696b 100644 --- a/lib/model/mine/user_data.dart +++ b/lib/model/mine/user_data.dart @@ -59,6 +59,7 @@ class UserData { final String? hometownProvinceName; final String? hometownCityCode; final String? hometownCityName; + bool? realAuth; UserData({ required this.id, @@ -120,6 +121,7 @@ class UserData { this.hometownProvinceName, this.hometownCityCode, this.hometownCityName, + this.realAuth, }); // 从JSON映射创建实例 @@ -184,6 +186,7 @@ class UserData { hometownProvinceName: json['hometownProvinceName'], hometownCityCode: json['hometownCityCode'], hometownCityName: json['hometownCityName'], + realAuth: json['realAuth'], ); } @@ -249,6 +252,7 @@ class UserData { 'hometownProvinceName': hometownProvinceName, 'hometownCityCode': hometownCityCode, 'hometownCityName': hometownCityName, + 'realAuth': realAuth, }; } diff --git a/lib/network/api_urls.dart b/lib/network/api_urls.dart index c939f15..39cb2f5 100644 --- a/lib/network/api_urls.dart +++ b/lib/network/api_urls.dart @@ -12,7 +12,7 @@ class ApiUrls { static const String getHxUserToken = 'dating-agency-chat-audio/user/get/hx/user/token'; static const String getApplyTempAuth = 'dating-agency-uec/get/apply-temp-auth'; static const String saveCertificationAudit = 'dating-agency-service/user/save/certification/audit'; - + static const String getCertificationList = '/dating-agency-service/user/get/certification/item/all/list'; //首页相关接口 static const String getMarriageList = 'dating-agency-service/user/page/dongwo/marriage-information'; diff --git a/lib/network/user_api.dart b/lib/network/user_api.dart index 5cd5f5d..299ba83 100644 --- a/lib/network/user_api.dart +++ b/lib/network/user_api.dart @@ -7,6 +7,8 @@ import 'package:dating_touchme_app/network/api_urls.dart'; import 'package:retrofit/retrofit.dart'; import 'package:dio/dio.dart'; +import '../model/mine/authentication_data.dart'; + part 'user_api.g.dart'; @RestApi(baseUrl: '') @@ -41,6 +43,11 @@ abstract class UserApi { @Body() Map data, ); + @GET(ApiUrls.getCertificationList) + Future>>> getCertificationList( + @Query('miId') String miId, + ); + @GET(ApiUrls.getHxUserToken) Future>> getHxUserToken(); diff --git a/lib/network/user_api.g.dart b/lib/network/user_api.g.dart index 18b97e6..5835815 100644 --- a/lib/network/user_api.g.dart +++ b/lib/network/user_api.g.dart @@ -220,6 +220,48 @@ class _UserApi implements UserApi { return httpResponse; } + @override + Future>>> + getCertificationList(String miId) async { + final _extra = {}; + final queryParameters = {r'miId': miId}; + final _headers = {}; + const Map? _data = null; + final _options = + _setStreamType>>>( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + '/dating-agency-service/user/get/certification/item/all/list', + queryParameters: queryParameters, + data: _data, + ) + .copyWith( + baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl), + ), + ); + final _result = await _dio.fetch>(_options); + late BaseResponse> _value; + try { + _value = BaseResponse>.fromJson( + _result.data!, + (json) => json is List + ? json + .map( + (i) => + AuthenticationData.fromJson(i as Map), + ) + .toList() + : List.empty(), + ); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + @override Future>> getHxUserToken() async { final _extra = {}; diff --git a/lib/pages/message/chat_page.dart b/lib/pages/message/chat_page.dart index 3c9f60a..a7bbeed 100644 --- a/lib/pages/message/chat_page.dart +++ b/lib/pages/message/chat_page.dart @@ -10,15 +10,48 @@ import '../../generated/assets.dart'; import '../../../widget/message/chat_input_bar.dart'; import '../../../widget/message/message_item.dart'; -class ChatPage extends StatelessWidget { +class ChatPage extends StatefulWidget { final String userId; const ChatPage({required this.userId, super.key}); + @override + State createState() => _ChatPageState(); +} + +class _ChatPageState extends State { + final ScrollController _scrollController = ScrollController(); + bool _isLoadingMore = false; + late ChatController _controller; + + @override + void initState() { + super.initState(); + // 初始化 controller + _controller = Get.put(ChatController(userId: widget.userId)); + + // 监听滚动,当滚动到顶部时加载更多消息 + _scrollController.addListener(() { + if (_scrollController.hasClients && + _scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 100 && + !_isLoadingMore && + _controller.messages.isNotEmpty) { + _loadMoreMessages(); + } + }); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return GetBuilder( - init: ChatController(userId: userId), + init: _controller, builder: (controller) { return WillPopScope( onWillPop: () async { @@ -57,6 +90,7 @@ class ChatPage extends StatelessWidget { }, behavior: HitTestBehavior.opaque, child: ListView.builder( + controller: _scrollController, reverse: true, padding: EdgeInsets.all(16.w), itemCount: controller.messages.length, @@ -79,34 +113,53 @@ class ChatPage extends StatelessWidget { ), ), ), - // 使用抽离的聊天输入栏组件 - ChatInputBar( - onSendMessage: (message) async { - await controller.sendMessage(message); - }, - onImageSelected: (imagePaths) async { - // 为每个图片路径调用控制器的方法发送图片消息 - for (var imagePath in imagePaths) { - await controller.sendImageMessage(imagePath); - } - }, - onVoiceRecorded: (filePath, seconds) async { - // 处理语音录音完成,回传文件路径和秒数 - await controller.sendVoiceMessage(filePath, seconds); - }, - onVideoRecorded: (filePath, duration) async { - print('🎬 [ChatPage] 收到视频录制/选择回调'); - print('文件路径: $filePath'); - print('时长: $duration 秒'); - // 处理视频录制/选择完成,回传文件路径和时长 - await controller.sendVideoMessage(filePath, duration); - }, - ), - ], + // 使用抽离的聊天输入栏组件 + ChatInputBar( + onSendMessage: (message) async { + await controller.sendMessage(message); + }, + onImageSelected: (imagePaths) async { + // 为每个图片路径调用控制器的方法发送图片消息 + for (var imagePath in imagePaths) { + await controller.sendImageMessage(imagePath); + } + }, + onVoiceRecorded: (filePath, seconds) async { + // 处理语音录音完成,回传文件路径和秒数 + await controller.sendVoiceMessage(filePath, seconds); + }, + onVideoRecorded: (filePath, duration) async { + print('🎬 [ChatPage] 收到视频录制/选择回调'); + print('文件路径: $filePath'); + print('时长: $duration 秒'); + // 处理视频录制/选择完成,回传文件路径和时长 + await controller.sendVideoMessage(filePath, duration); + }, + ), + ], + ), ), - ), - ); + ); }, ); } + + // 加载更多消息 + Future _loadMoreMessages() async { + if (_isLoadingMore) return; + + setState(() { + _isLoadingMore = true; + }); + + try { + await _controller.fetchMessages(loadMore: true); + } finally { + if (mounted) { + setState(() { + _isLoadingMore = false; + }); + } + } + } } diff --git a/lib/pages/mine/auth_center_page.dart b/lib/pages/mine/auth_center_page.dart new file mode 100644 index 0000000..e3033bb --- /dev/null +++ b/lib/pages/mine/auth_center_page.dart @@ -0,0 +1,126 @@ +import 'package:dating_touchme_app/extension/ex_widget.dart'; +import 'package:dating_touchme_app/pages/mine/real_name_page.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../controller/mine/auth_controller.dart'; +import 'edit_info_page.dart'; + +class AuthCenterPage extends StatelessWidget { + AuthCenterPage({super.key}); + final AuthController controller = Get.put(AuthController()); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Color(0xffF5F5F5), + appBar: AppBar( + title: Text('认证中心', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + centerTitle: true, + leading: IconButton( + icon: Icon(Icons.arrow_back_ios, size: 24, color: Colors.grey,), + onPressed: () { + Get.back(); + }, + ), + ), + body: Obx(() { + if (controller.isLoading.value) { + return const Center(child: CupertinoActivityIndicator(radius: 12,)); + } + return ListView.builder( + padding: const EdgeInsets.only(top: 16, right: 16, left: 16), + itemCount: controller.dataList.length, + itemBuilder: (context, index) { + final record = controller.dataList[index]; + return _buildListItem(record); + }, + ); + }) + ); + } + + // 构建列表项 + Widget _buildListItem(AuthCard item) { + return Container( + margin: EdgeInsets.only(bottom: 12), + padding: EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // 左侧图片 + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Colors.blue[100], + image: DecorationImage( + image: NetworkImage('https://picsum.photos/40/40?random=$item.index'), + fit: BoxFit.cover, + ), + ), + ), + SizedBox(width: 12), + // 右侧内容 + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.grey[800], + ), + ), + SizedBox(height: 2), + Text( + item.desc, + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + Spacer(), + Row( + children: [ + Text( + item.authed ? '已认证' : '去认证', + style: TextStyle( + fontSize: 12, + color: item.authed ? Color(0xff26C77C) : Colors.grey[500] + ) + ), + SizedBox(width: 4), + item.authed ? SizedBox(width: 24) : Icon( + Icons.navigate_next, // Material Icons + // size: 128.0, // 设置图标大小#26C77C + color: Colors.grey[500] + ), + ], + ) + ], + ), + ).onTap(() async{ + if(!item.authed){ + if(item.index == 2){ + Get.to(() => EditInfoPage()); + } else if(item.index == 3){ + final result = await Get.to(() => RealNamePage()); + print(result); + } + } + }); + } + +} diff --git a/lib/pages/mine/login_page.dart b/lib/pages/mine/login_page.dart index 126daaf..6facb6c 100644 --- a/lib/pages/mine/login_page.dart +++ b/lib/pages/mine/login_page.dart @@ -30,7 +30,6 @@ class LoginPage extends StatelessWidget { child: Column( children: [ const SizedBox(height: 150), - // Logo和标题区域 Center( child: Column( diff --git a/lib/pages/mine/real_name_page.dart b/lib/pages/mine/real_name_page.dart new file mode 100644 index 0000000..aaadc8c --- /dev/null +++ b/lib/pages/mine/real_name_page.dart @@ -0,0 +1,196 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; +import 'package:tdesign_flutter/tdesign_flutter.dart'; + +import '../../controller/mine/auth_controller.dart'; + +class RealNamePage extends StatelessWidget { + RealNamePage({super.key}); + final AuthController controller = Get.put(AuthController()); +// 是否同意协议 + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Color(0xffFFFFFF), + appBar: AppBar( + title: Text('实名认证', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + centerTitle: true, + leading: IconButton( + icon: Icon(Icons.arrow_back_ios, size: 24, color: Colors.grey,), + onPressed: () { + Get.back(); + }, + ), + ), + body: Column( + children: [ + Container( + height: 48, + decoration: BoxDecoration(color: Color(0xffE7E7E7)), + padding: const EdgeInsets.only(left: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, // 垂直居中 + children: [ + Text( + '*请填写本人实名信息', + style: TextStyle( + fontSize: 14, + color: Colors.black87, + ), + ), + ], + ), + ), + Container( + height: 56, // 固定高度确保垂直居中 + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.grey[400]!, + width: 0.5, + ), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, // 垂直居中 + children: [ + // 左侧标签 - 固定宽度 + 垂直居中 + Container( + width: 100, + alignment: Alignment.centerLeft, + padding: const EdgeInsets.only(left: 16), + child: Text( + '姓名:', + style: TextStyle( + fontSize: 15, + color: Colors.black87, + ), + ), + ), + SizedBox(width: 12), + + // 输入框区域 - 使用Expanded填充剩余空间 + Expanded( + child: Container( + alignment: Alignment.centerLeft, // 输入框内容垂直居中 + child: TextField( + decoration: InputDecoration( + hintText: '请输入姓名', + hintStyle: TextStyle(color: Colors.grey[500]), + border: InputBorder.none, // 隐藏默认边框 + contentPadding: EdgeInsets.zero, // 去除默认padding + isDense: true, // 紧凑模式 + ), + style: TextStyle( + fontSize: 15, + height: 1.2, // 控制文字垂直位置 + ), + onChanged: (value) { + controller.name.value = value; + }, + ), + ), + ), + ], + ), + ), + // SizedBox(height: 30), + Container( + height: 56, // 固定高度确保垂直居中 + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.grey[400]!, + width: 0.5, + ), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, // 垂直居中 + children: [ + // 左侧标签 - 固定宽度 + 垂直居中 + Container( + width: 100, + alignment: Alignment.centerLeft, + padding: const EdgeInsets.only(left: 16), + child: Text( + '身份证号:', + style: TextStyle( + fontSize: 15, + color: Colors.black87, + ), + ), + ), + SizedBox(width: 12), + + // 输入框区域 - 使用Expanded填充剩余空间 + Expanded( + child: Container( + alignment: Alignment.centerLeft, // 输入框内容垂直居中 + child: TextField( + decoration: InputDecoration( + hintText: '请输入身份证号', + hintStyle: TextStyle(color: Colors.grey[500]), + border: InputBorder.none, // 隐藏默认边框 + contentPadding: EdgeInsets.zero, // 去除默认padding + isDense: true, // 紧凑模式 + ), + style: TextStyle( + fontSize: 15, + height: 1.2, // 控制文字垂直位置 + ), + onChanged: (value) { + controller.idcard.value = value; + }, + ), + ), + ), + ], + ), + ), + SizedBox(height: 24), + // 协议同意复选框 + Row( + crossAxisAlignment: CrossAxisAlignment.start, // 垂直居中 + children: [ + SizedBox(width: 8), + Obx(() => Checkbox( + value: controller.agree.value, + onChanged: (value) { + controller.agree.value = value ?? false; + }, + activeColor: Color(0xff7562F9), + side: const BorderSide(color: Colors.grey), + shape: const CircleBorder(), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + )), + ConstrainedBox( + constraints: BoxConstraints(maxWidth: 300), // 限制最大宽度 + child: Text( + '依据法律法规的要求,我们将收集您的真实姓名,身份证号用于实名认证,认证信息将用于直播连麦、收益提现、依据证件信息更正性别等,与账号唯一绑定,我们会使用加密方式对您的认证信息进行严格保密。', + style: TextStyle( fontSize: 13, color: Colors.grey ), + ), + ) + ], + ), + SizedBox(height: 48), + + TDButton( + text: '立即认证', + width: MediaQuery.of(context).size.width - 40, + size: TDButtonSize.large, + type: TDButtonType.fill, + shape: TDButtonShape.round, + theme: TDButtonTheme.primary, + onTap: (){ + controller.startAuthing(); + }, + ), + ], + ), + ); + } + +} diff --git a/lib/rtc/rtc_manager.dart b/lib/rtc/rtc_manager.dart new file mode 100644 index 0000000..c89abe9 --- /dev/null +++ b/lib/rtc/rtc_manager.dart @@ -0,0 +1,425 @@ +import 'package:agora_rtc_engine/agora_rtc_engine.dart'; + +/// RTC 管理器,负责管理声网音视频通话功能 +class RTCManager { + // 单例模式 + static final RTCManager _instance = RTCManager._internal(); + factory RTCManager() => _instance; + // 静态getter用于instance访问 + static RTCManager get instance => _instance; + + RtcEngine? _engine; + bool _isInitialized = false; + bool _isInChannel = false; + String? _currentChannelId; + int? _currentUid; + + // 事件回调 + Function(RtcConnection connection, int elapsed)? onJoinChannelSuccess; + Function(RtcConnection connection, int remoteUid, int elapsed)? onUserJoined; + Function( + RtcConnection connection, + int remoteUid, + UserOfflineReasonType reason, + )? + onUserOffline; + Function(RtcConnection connection, RtcStats stats)? onLeaveChannel; + Function( + RtcConnection connection, + ConnectionStateType state, + ConnectionChangedReasonType reason, + )? + onConnectionStateChanged; + Function(int uid, UserInfo userInfo)? onUserInfoUpdated; + Function(RtcConnection connection, int uid, int elapsed)? + onFirstRemoteVideoDecoded; + Function( + RtcConnection connection, + VideoSourceType sourceType, + int uid, + int width, + int height, + int rotation, + )? + onVideoSizeChanged; + Function(RtcConnection connection, int uid, bool muted)? onUserMuteAudio; + Function(RtcConnection connection, int uid, bool muted)? onUserMuteVideo; + Function(RtcConnection connection)? onConnectionLost; + Function(RtcConnection connection, int code, String msg)? onError; + + RTCManager._internal() { + print('RTCManager instance created'); + } + + /// 初始化 RTC Engine + /// [appId] 声网 App ID + /// [channelProfile] 频道场景类型,默认为通信模式 + Future initialize({ + required String appId, + ChannelProfileType channelProfile = + ChannelProfileType.channelProfileCommunication, + }) async { + try { + if (_isInitialized && _engine != null) { + print('RTC Engine already initialized'); + return true; + } + + // 创建 RTC Engine + _engine = createAgoraRtcEngine(); + + // 初始化 RTC Engine + await _engine!.initialize( + RtcEngineContext(appId: appId, channelProfile: channelProfile), + ); + + // 注册事件处理器 + _registerEventHandlers(); + + _isInitialized = true; + print('RTC Engine initialized successfully'); + return true; + } catch (e) { + print('Failed to initialize RTC Engine: $e'); + return false; + } + } + + /// 注册事件处理器 + void _registerEventHandlers() { + if (_engine == null) return; + + _engine!.registerEventHandler( + RtcEngineEventHandler( + onJoinChannelSuccess: (RtcConnection connection, int elapsed) { + _isInChannel = true; + _currentChannelId = connection.channelId; + print('加入频道成功,频道名:${connection.channelId},耗时:${elapsed}ms'); + if (onJoinChannelSuccess != null) { + onJoinChannelSuccess!(connection, elapsed); + } + }, + onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) { + print('用户加入,UID:$remoteUid'); + if (onUserJoined != null) { + onUserJoined!(connection, remoteUid, elapsed); + } + }, + onUserOffline: + ( + RtcConnection connection, + int remoteUid, + UserOfflineReasonType reason, + ) { + print('用户离开,UID:$remoteUid,原因:$reason'); + if (onUserOffline != null) { + onUserOffline!(connection, remoteUid, reason); + } + }, + onLeaveChannel: (RtcConnection connection, RtcStats stats) { + _isInChannel = false; + _currentChannelId = null; + print('离开频道,统计信息:${stats.duration}秒'); + if (onLeaveChannel != null) { + onLeaveChannel!(connection, stats); + } + }, + onConnectionStateChanged: + ( + RtcConnection connection, + ConnectionStateType state, + ConnectionChangedReasonType reason, + ) { + print('连接状态改变:$state,原因:$reason'); + if (onConnectionStateChanged != null) { + onConnectionStateChanged!(connection, state, reason); + } + }, + onUserInfoUpdated: (int uid, UserInfo userInfo) { + print('用户信息更新,UID:$uid'); + if (onUserInfoUpdated != null) { + onUserInfoUpdated!(uid, userInfo); + } + }, + onFirstRemoteVideoDecoded: + ( + RtcConnection connection, + int uid, + int width, + int height, + int elapsed, + ) { + print('首次远程视频解码,UID:$uid,分辨率:${width}x${height}'); + if (onFirstRemoteVideoDecoded != null) { + onFirstRemoteVideoDecoded!(connection, uid, elapsed); + } + }, + onVideoSizeChanged: + ( + RtcConnection connection, + VideoSourceType sourceType, + int uid, + int width, + int height, + int rotation, + ) { + print('视频尺寸改变,UID:$uid,分辨率:${width}x${height}'); + if (onVideoSizeChanged != null) { + onVideoSizeChanged!( + connection, + sourceType, + uid, + width, + height, + rotation, + ); + } + }, + onUserMuteAudio: (RtcConnection connection, int uid, bool muted) { + print('用户静音状态改变,UID:$uid,静音:$muted'); + if (onUserMuteAudio != null) { + onUserMuteAudio!(connection, uid, muted); + } + }, + onUserMuteVideo: (RtcConnection connection, int uid, bool muted) { + print('用户视频状态改变,UID:$uid,关闭:$muted'); + if (onUserMuteVideo != null) { + onUserMuteVideo!(connection, uid, muted); + } + }, + onConnectionLost: (RtcConnection connection) { + print('连接丢失'); + if (onConnectionLost != null) { + onConnectionLost!(connection); + } + }, + onError: (ErrorCodeType err, String msg) { + print('RTC Engine 错误:$err,消息:$msg'); + if (onError != null) { + onError!( + RtcConnection( + channelId: _currentChannelId ?? '', + localUid: _currentUid ?? 0, + ), + err.value(), + msg, + ); + } + }, + ), + ); + } + + /// 启用视频 + Future enableVideo() async { + if (_engine == null) { + throw Exception('RTC Engine not initialized'); + } + await _engine!.enableVideo(); + print('视频已启用'); + } + + /// 禁用视频 + Future disableVideo() async { + if (_engine == null) { + throw Exception('RTC Engine not initialized'); + } + await _engine!.disableVideo(); + print('视频已禁用'); + } + + /// 启用音频 + Future enableAudio() async { + if (_engine == null) { + throw Exception('RTC Engine not initialized'); + } + await _engine!.enableAudio(); + print('音频已启用'); + } + + /// 禁用音频 + Future disableAudio() async { + if (_engine == null) { + throw Exception('RTC Engine not initialized'); + } + await _engine!.disableAudio(); + print('音频已禁用'); + } + + /// 开启本地视频预览 + Future startPreview() async { + if (_engine == null) { + throw Exception('RTC Engine not initialized'); + } + await _engine!.startPreview(); + print('本地视频预览已开启'); + } + + /// 停止本地视频预览 + Future stopPreview() async { + if (_engine == null) { + throw Exception('RTC Engine not initialized'); + } + await _engine!.stopPreview(); + print('本地视频预览已停止'); + } + + /// 设置本地视频视图 + /// [viewId] 视图ID + /// [mirrorMode] 镜像模式 + Future setupLocalVideo({ + required int viewId, + VideoSourceType sourceType = VideoSourceType.videoSourceCameraPrimary, + VideoMirrorModeType mirrorMode = VideoMirrorModeType.videoMirrorModeAuto, + }) async { + if (_engine == null) { + throw Exception('RTC Engine not initialized'); + } + await _engine!.setupLocalVideo( + VideoCanvas(view: viewId, sourceType: sourceType, mirrorMode: mirrorMode), + ); + print('本地视频视图已设置,viewId:$viewId'); + } + + /// 设置远程视频视图 + /// [uid] 远程用户ID + /// [viewId] 视图ID + Future setupRemoteVideo({ + required int uid, + required int viewId, + VideoSourceType sourceType = VideoSourceType.videoSourceCameraPrimary, + VideoMirrorModeType mirrorMode = + VideoMirrorModeType.videoMirrorModeDisabled, + }) async { + if (_engine == null) { + throw Exception('RTC Engine not initialized'); + } + await _engine!.setupRemoteVideo( + VideoCanvas( + uid: uid, + view: viewId, + sourceType: sourceType, + mirrorMode: mirrorMode, + ), + ); + print('远程视频视图已设置,UID:$uid,viewId:$viewId'); + } + + /// 加入频道 + /// [token] 频道令牌(可选,如果频道未开启鉴权则可以为空字符串) + /// [channelId] 频道ID + /// [uid] 用户ID(0表示自动分配) + /// [options] 频道媒体选项 + Future joinChannel({ + String? token, + required String channelId, + int uid = 0, + ChannelMediaOptions? options, + }) async { + if (_engine == null) { + throw Exception('RTC Engine not initialized'); + } + if (_isInChannel) { + print('已经在频道中,先离开当前频道'); + await leaveChannel(); + } + + _currentUid = uid; + await _engine!.joinChannel( + token: token ?? '', + channelId: channelId, + uid: uid, + options: options ?? const ChannelMediaOptions(), + ); + print('正在加入频道:$channelId,UID:$uid'); + } + + /// 离开频道 + Future leaveChannel() async { + if (_engine == null) { + throw Exception('RTC Engine not initialized'); + } + if (!_isInChannel) { + print('当前不在频道中'); + return; + } + + await _engine!.leaveChannel(); + _currentUid = null; + print('已离开频道'); + } + + /// 切换摄像头 + Future switchCamera() async { + if (_engine == null) { + throw Exception('RTC Engine not initialized'); + } + await _engine!.switchCamera(); + print('摄像头已切换'); + } + + /// 静音/取消静音本地音频 + /// [muted] true表示静音,false表示取消静音 + Future muteLocalAudio(bool muted) async { + if (_engine == null) { + throw Exception('RTC Engine not initialized'); + } + await _engine!.muteLocalAudioStream(muted); + print('本地音频${muted ? "已静音" : "已取消静音"}'); + } + + /// 开启/关闭本地视频 + /// [enabled] true表示开启,false表示关闭 + Future muteLocalVideo(bool enabled) async { + if (_engine == null) { + throw Exception('RTC Engine not initialized'); + } + await _engine!.muteLocalVideoStream(!enabled); + print('本地视频${enabled ? "已开启" : "已关闭"}'); + } + + /// 设置客户端角色(仅用于直播场景) + /// [role] 客户端角色:主播或观众 + Future setClientRole({ + required ClientRoleType role, + ClientRoleOptions? options, + }) async { + if (_engine == null) { + throw Exception('RTC Engine not initialized'); + } + await _engine!.setClientRole(role: role, options: options); + print('客户端角色已设置为:$role'); + } + + /// 获取当前是否在频道中 + bool get isInChannel => _isInChannel; + + /// 获取当前频道ID + String? get currentChannelId => _currentChannelId; + + /// 获取当前用户ID + int? get currentUid => _currentUid; + + /// 获取 RTC Engine 实例(用于高级操作) + RtcEngine? get engine => _engine; + + /// 释放资源 + Future dispose() async { + try { + if (_isInChannel) { + await leaveChannel(); + } + if (_engine != null) { + await _engine!.release(); + _engine = null; + } + _isInitialized = false; + _isInChannel = false; + _currentChannelId = null; + _currentUid = null; + print('RTC Engine disposed'); + } catch (e) { + print('Failed to dispose RTC Engine: $e'); + } + } +} diff --git a/lib/widget/message/voice_item.dart b/lib/widget/message/voice_item.dart index 3e91478..6fc1656 100644 --- a/lib/widget/message/voice_item.dart +++ b/lib/widget/message/voice_item.dart @@ -1,3 +1,6 @@ +import 'dart:io'; +import 'dart:math' as math; +import 'package:audioplayers/audioplayers.dart'; import 'package:dating_touchme_app/extension/ex_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -28,25 +31,67 @@ class VoiceItem extends StatefulWidget { State createState() => _VoiceItemState(); } -class _VoiceItemState extends State { +class _VoiceItemState extends State with TickerProviderStateMixin { final VoicePlayerManager _playerManager = VoicePlayerManager.instance; + late AnimationController _waveformAnimationController; + int _animationFrame = 0; @override void initState() { super.initState(); + // 创建波形动画控制器 + _waveformAnimationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 2000), // 2秒完成一个波浪周期 + ); + + // 监听动画帧,更新波形 + _waveformAnimationController.addListener(() { + if (mounted && _playerManager.isPlaying(widget.messageId)) { + setState(() { + // 使用动画值来计算波浪位置,让波浪从左到右传播 + _animationFrame++; + }); + } + }); + // 监听播放状态变化 ever(_playerManager.currentPlayingId, (audioId) { if (mounted) { setState(() {}); + // 根据播放状态控制动画 + if (_playerManager.isPlaying(widget.messageId)) { + if (!_waveformAnimationController.isAnimating) { + _waveformAnimationController.repeat(); + } + } else { + _waveformAnimationController.stop(); + _waveformAnimationController.reset(); + _animationFrame = 0; + } } }); ever(_playerManager.playerState, (state) { if (mounted) { setState(() {}); + // 根据播放状态控制动画 + if (state == PlayerState.playing && + _playerManager.currentPlayingId.value == widget.messageId) { + _waveformAnimationController.repeat(); + } else { + _waveformAnimationController.stop(); + _animationFrame = 0; + } } }); } + @override + void dispose() { + _waveformAnimationController.dispose(); + super.dispose(); + } + // 处理播放/暂停 Future _handlePlayPause() async { try { @@ -55,19 +100,37 @@ class _VoiceItemState extends State { final localPath = widget.voiceBody.localPath; final remotePath = widget.voiceBody.remotePath; - if (localPath.isNotEmpty) { - filePath = localPath; - } else if (remotePath != null && remotePath.isNotEmpty) { - // 如果是远程路径,需要先下载(这里简化处理,实际应该先下载到本地) - filePath = remotePath; + // 优先使用本地路径 + if (remotePath != null && remotePath.isNotEmpty) { + // 只有远程路径,尝试使用远程路径(audioplayers 可能支持网络URL) + // 注意:如果远程路径是文件系统路径而不是URL,需要先下载 + if (remotePath.startsWith('http://') || + remotePath.startsWith('https://')) { + filePath = remotePath; + } else { + SmartDialog.showToast('音频文件不存在,请等待下载完成'); + print('远程音频文件路径: $remotePath'); + return; + } + } else if (localPath.isNotEmpty) { + final localFile = File(localPath); + if (await localFile.exists()) { + filePath = localPath; + } else { + SmartDialog.showToast('音频文件不存在'); + print('本地音频文件不存在: $localPath'); + return; + } } - SmartDialog.showToast('来了$remotePath'); + if (filePath != null && filePath.isNotEmpty) { await _playerManager.play(widget.messageId, filePath); } else { + SmartDialog.showToast('无法获取音频文件'); print('音频文件路径为空'); } } catch (e) { + SmartDialog.showToast('播放失败: $e'); print('播放音频失败: $e'); } } @@ -120,9 +183,7 @@ class _VoiceItemState extends State { height: 20.w, decoration: BoxDecoration( shape: BoxShape.circle, - color: widget.isSentByMe - ? Colors.white - : Colors.black, + color: widget.isSentByMe ? Colors.white : Colors.black, ), child: Icon( isPlaying ? Icons.pause : Icons.play_arrow, @@ -193,6 +254,7 @@ class _VoiceItemState extends State { Widget _buildWaveform() { // 根据时长生成波形条数量(最多20个) final barCount = (widget.voiceBody.duration / 2).ceil().clamp(5, 20); + final isPlaying = _playerManager.isPlaying(widget.messageId); return SizedBox( height: 16.h, @@ -200,19 +262,44 @@ class _VoiceItemState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: List.generate(barCount, (index) { - // 模拟波形高度变化 - final random = (index * 7) % 5; - final baseHeight = 6 + random * 2; - final height = (baseHeight.clamp(4, 16)).h; + double height; + Color color; + + if (isPlaying) { + // 播放时:波浪形动画效果 + // 使用正弦波函数创建波浪效果,从左到右传播 + // wavePhase: 时间因子(_animationFrame)让波浪移动,空间因子(index)让每个条的位置不同 + // 移除模运算,让波浪连续传播 + final wavePhase = _animationFrame * 0.15 + index * 0.6; + // 使用正弦波计算高度,范围在 4-16 之间 + final sinValue = math.sin(wavePhase); + final normalizedValue = (sinValue + 1) / 2; // 归一化到 0-1 + final baseHeight = 4 + normalizedValue * 12; + height = (baseHeight.clamp(4, 16)).h; + + // 根据高度设置颜色透明度,创造渐变效果 + final opacity = 0.5 + normalizedValue * 0.5; + color = widget.isSentByMe + ? Colors.white.withOpacity(opacity.clamp(0.5, 1.0)) + : Colors.grey.withOpacity(opacity.clamp(0.5, 0.9)); + } else { + // 未播放时:静态波形 + final random = (index * 7) % 5; + final baseHeight = 6 + random * 2; + height = (baseHeight.clamp(4, 16)).h; + color = widget.isSentByMe + ? Colors.white.withOpacity(0.8) + : Colors.grey.withOpacity(0.6); + } - return Container( + return AnimatedContainer( + duration: const Duration(milliseconds: 100), + curve: Curves.easeOut, width: 2.w, height: height, margin: EdgeInsets.symmetric(horizontal: 1.w), decoration: BoxDecoration( - color: widget.isSentByMe - ? Colors.white.withOpacity(0.8) - : Colors.grey.withOpacity(0.6), + color: color, borderRadius: BorderRadius.circular(1.w), ), ); diff --git a/pubspec.lock b/pubspec.lock index 78ec962..3389ecc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "88.0.0" + agora_rtc_engine: + dependency: "direct main" + description: + name: agora_rtc_engine + sha256: "6559294d18ce4445420e19dbdba10fb58cac955cd8f22dbceae26716e194d70e" + url: "https://pub.dev" + source: hosted + version: "6.5.3" analyzer: dependency: transitive description: @@ -573,6 +581,14 @@ packages: description: flutter source: sdk version: "0.0.0" + fluwx: + dependency: "direct main" + description: + name: fluwx + sha256: "7e92d2000ee49c5262a88c51ea2d22b91a753d5b29df27cc264bb0a115d65373" + url: "https://pub.dev" + source: hosted + version: "5.7.5" frontend_server_client: dependency: transitive description: @@ -781,6 +797,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + iris_method_channel: + dependency: transitive + description: + name: iris_method_channel + sha256: bfb5cfc6c6eae42da8cd1b35977a72d8b8881848a5dfc3d672e4760a907d11a0 + url: "https://pub.dev" + source: hosted + version: "2.2.4" js: dependency: transitive description: @@ -1466,6 +1490,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.6" + tobias: + dependency: "direct main" + description: + name: tobias + sha256: "2b5520e622c0d6f04cfb5c9619211f923c97a602e1a3a8954e113e3e0e685c41" + url: "https://pub.dev" + source: hosted + version: "5.3.1" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 769450e..f38e319 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,6 +62,9 @@ dependencies: video_player: ^2.9.2 chewie: ^1.8.5 # 视频播放器UI audioplayers: ^6.5.1 + fluwx: ^5.7.5 + tobias: ^5.3.1 + agora_rtc_engine: ^6.5.3 dev_dependencies: flutter_test: