diff --git a/lib/data/services/db/sqflite_api.dart b/lib/data/services/db/sqflite_api.dart index a400298fb..45fc7f898 100644 --- a/lib/data/services/db/sqflite_api.dart +++ b/lib/data/services/db/sqflite_api.dart @@ -293,6 +293,7 @@ class SqfliteApi { limit: 4, where: 'categoryid = ?', whereArgs: [element['id']], + orderBy: 'id ${element['name'] == '自定义' ? 'DESC' : 'ASC'}', ); element['template'] = templateList .map((item) => { @@ -305,12 +306,13 @@ class SqfliteApi { } // 获取商务会议助手模版列表 - static Future getMeetingTemplateList(int id) async { + static Future getMeetingTemplateList(int id, {bool isCustom = false}) async { final db = await DatabaseHelper().database; return await db.query( 'meetingtemplate', where: 'categoryid = ?', whereArgs: [id], + orderBy: 'id ${isCustom ? 'DESC' : 'ASC'}', ); } diff --git a/lib/data/services/meeting/meeting_task_service.dart b/lib/data/services/meeting/meeting_task_service.dart index 50b7a5def..7ec049fc7 100644 --- a/lib/data/services/meeting/meeting_task_service.dart +++ b/lib/data/services/meeting/meeting_task_service.dart @@ -10,29 +10,35 @@ import '../db/sqflite_api.dart'; import '../deapsound_ai_service.dart'; import 'meeting_upload_service.dart'; +///会议任务服务类,负责管理会议音频的转写任务处理流程,包括任务提交、状态轮询、结果处理和AI分析等功能。 class MeetingTaskService extends GetxService { final GetStorage _storage = GetStorage(); MeetingUploadService get _uploadService => Get.find(); - List _taskList = []; - RxList taskFinishList = [].obs; + List _taskList = []; //待处理的会议任务列表 + RxList taskFinishList = [].obs; //已处理的会议任务列表 - bool _isTask = false; + bool _isTask = false; //是否有正在处理的任务 String _userId = ''; +//字节跳动(火山引擎)语音服务配置 final String _appid = '2316081933'; final String _token = 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En'; - final String _cluster = 'volc_auc_tele_flash'; - final String _serviceUrl = 'https://openspeech.bytedance.com/api/v1/auc'; + final String _cluster = 'volc_auc_video_flash'; + final String _cluster1 = 'volc.bigasr.auc'; + final String _serviceUrl = 'https://openspeech.bytedance.com/api/v1/auc'; // + final String _serviceUrl1 = + 'https://openspeech.bytedance.com/api/v3/auc/bigmodel'; //大模型录音文件识别api地址 Map _headers = {}; + Map _headers1 = {}; int meetingId = 0; - RxInt tasktype = 1.obs; - List textList = []; - String summary = ''; - String overview = ''; + RxInt tasktype = 1.obs; //任务类型,1-转写中,2-总结中,3-完成 + List textList = []; //转写结果列表 + String summary = ''; //AI总结内容 + String overview = ''; //AI概览内容 @override void onInit() { @@ -41,6 +47,14 @@ class MeetingTaskService extends GetxService { 'Authorization': 'Bearer; $_token', 'Content-Type': 'application/json', }; + _headers1 = { + 'X-Api-App-Key': _appid, + 'X-Api-Access-Key': _token, + 'X-Api-Resource-Id': _cluster1, + 'X-Api-Request-Id': '', + 'X-Api-Sequence': '-1', + }; + _getAgent(); _getTaskList(); } @@ -56,6 +70,14 @@ class MeetingTaskService extends GetxService { _executeTask(); } + ///添加会议任务 + ///[id] 会议ID + ///[audiourl] 音频URL + ///[taskid] 任务ID + ///[isSpeaker] 是否为发言人 + ///[language] 语言 + ///[prompt] 提示词 + ///[outline] 大纲 Future addTask(int id, String audiourl, String taskid, bool isSpeaker, String language, String prompt, String outline) async { if (audiourl.isEmpty) { @@ -81,6 +103,14 @@ class MeetingTaskService extends GetxService { } } + ///添加会议任务列表 + ///[id] 会议ID + ///[audiourl] 音频URL + ///[taskId] 任务ID + ///[isSpeaker] 是否为发言人 + ///[language] 语言 + ///[prompt] 提示词 + ///[outline] 大纲 Future addTaskList(int id, String audiourl, String taskId, bool isSpeaker, String language, String prompt, String outline) async { if (taskId.isEmpty) { @@ -103,14 +133,15 @@ class MeetingTaskService extends GetxService { _executeTask(); } + ///执行会议任务 void _executeTask() async { if (_isTask) return; _isTask = true; while (_taskList.isNotEmpty) { await Future.delayed(const Duration(seconds: 2)); - - for (var element in List.from(_taskList)) { - await _fileRecognize(element); + Logger.info("lxm--轮询---会议任务列表$_taskList"); + for (var task in List.from(_taskList)) { + await _fileRecognize(task); } } _isTask = false; @@ -133,9 +164,40 @@ class MeetingTaskService extends GetxService { } } +// Future _submitTask( String audiourl, bool isSpeaker, String language) async { String format = _getFileType(audiourl); + final taskId = DateTime.now().millisecondsSinceEpoch.toString(); + + _headers1['X-Api-Request-Id'] = taskId; + Logger.info("lxm--大模型asr请求头$_headers1"); + + final requestBody1 = jsonEncode({ + "user": { + "uid": _userId, + }, + "audio": { + "url": audiourl, + "language": language, + "format": format, + }, + "request": { + "model_name": "bigmodel", // 模型名称,目前只有bigmodel + "model_version": "400", // 模型版本,400模型性能略有提升,且ITN有较大优化 + //"enable_itn": true, // 启用文本规范化,默认为true + "enable_punc": true, // 启用标点,默认为false + //"enable_ddc": false, // 启用语义顺滑,默认为false + "enable_speaker_info": isSpeaker, // 启用说话人聚类分离,10人以内效果较好 + //"enable_channel_split": false, // 启用双声道识别,默认为false + //"show_utterances": true, // 输出语音停顿、分句、分词信息 + //"show_speech_rate": false, // 分句信息携带语速 + //"show_volume": false, // 分句信息携带音量 + //"enable_lid": false, // 启用语种识别 + } + }); + + Logger.info("lxm--大模型asr请求体$requestBody1"); final requestBody = jsonEncode({ "app": { @@ -158,18 +220,24 @@ class MeetingTaskService extends GetxService { try { final response = await http.post( - Uri.parse('$_serviceUrl/submit'), - headers: _headers, - body: requestBody, + Uri.parse('$_serviceUrl1/submit'), + headers: _headers1, + body: requestBody1, ); + Logger.info("lxm--大模型asr响应体${response.body}"); + Logger.info("lxm--大模型asr上传响应头${response.headers['x-tt-logid']}"); + Logger.info( + "lxm--大模型asr上传服务端返回的状态码${response.headers['x-api-status-code']}"); + Logger.info("lxm--大模型asr上传服务端返回的消息${response.headers['x-api-message']}"); + if (response.statusCode != 200) { throw Exception('Submit failed with status: ${response.statusCode}'); } - final respDic = jsonDecode(response.body); - final taskId = respDic['resp']['id'] as String; - Logger.debug('taskId: $taskId'); + //final respDic = jsonDecode(response.body); + //final taskId = respDic['resp']['id'] as String; + //Logger.debug('taskId: $taskId'); return taskId; } catch (e) { Logger.error('Submit error: ${e.toString()}'); @@ -182,6 +250,8 @@ class MeetingTaskService extends GetxService { return array[array.length - 1]; } + ///轮询语音转文字任务状态 + ///[taskId] 任务ID Future> _queryTask(String taskId) async { final queryBody = jsonEncode({ "appid": _appid, @@ -191,12 +261,20 @@ class MeetingTaskService extends GetxService { }); try { + _headers1['X-Api-Request-Id'] = taskId; final response = await http.post( - Uri.parse('$_serviceUrl/query'), - headers: _headers, - body: queryBody, + Uri.parse('$_serviceUrl1/query'), + headers: _headers1, + body: jsonEncode({}), ); + // Logger.info("lxm---大模型asr响应体${response.body}"); + // Logger.info("lxm---大模型asr响应头${response.headers}"); + // Logger.info("lxm---大模型asr响应状态码${response.statusCode}"); + // Logger.info("lxm---大模型asr响应体logid${response.headers['x-tt-logid']}"); + // Logger.info("lxm---大模型asr服务端返回的状态码${response.headers['x-api-status-code']}"); + // Logger.info("lxm---大模型asr服务端返回的消息${response.headers['x-api-message']}"); + if (response.statusCode != 200) { throw Exception('Query failed with status: ${response.statusCode}'); } @@ -209,20 +287,24 @@ class MeetingTaskService extends GetxService { } } + ///轮询语音转文字任务状态,若任务完成则处理转写结果 + ///[item] 包含任务ID、会议ID、AI总结提示词、AI概览提示词的Map Future _fileRecognize(Map item) async { try { var respDic = await _queryTask(item['taskid']); - var code = respDic['resp']['code'] as int; + //var code = respDic['resp']['code'] as int; + + Logger.info("lxm---大模型asr响应体结果$respDic['result']['text']"); - if (code == 1000) { + if (respDic['result']['text'] != null) { await _addTextList( - item['id'], - respDic['resp']['utterances'], - item['prompt'], - item['outline'], + item['id'], //会议ID + respDic['result']['utterances'], //语音转文字结果 + item['prompt'], //AI总结提示词 + item['outline'], //AI概览提示词 ); return; - } else if (code < 2000) { + } else { Logger.debug("FileRecognize failed"); return; } @@ -231,11 +313,18 @@ class MeetingTaskService extends GetxService { } } + ///处理语音识别完成后的转写结果,包括数据库存储、状态更新和触发AI分析 + ///[id] 会议ID + ///[utterances] 语音转文字结果 + ///[prompt] AI总结提示词 + ///[outline] AI概览提示词 Future _addTextList( int id, List utterances, String prompt, String outline) async { String text = ''; String personnel = ''; List> list = []; + + // 处理转写结果,将每个句子的开始时间、结束时间、发言人和内容存储到列表中 for (var element in utterances) { String speaker = ''; if (element['additions'].isNotEmpty) { @@ -254,8 +343,10 @@ class MeetingTaskService extends GetxService { }); text += '${element['text']}\n'; } + // 存储处理后的转写结果到数据库 var result = await SqfliteApi.insertMeetingSpeaker(list); if (result != null) { + // 更新会议状态为“总结中” await SqfliteApi.editMeeting( id, {'tasktype': 2, 'personnel': personnel}, @@ -282,9 +373,16 @@ class MeetingTaskService extends GetxService { _storage.write("meeting_task_list", _taskList); _storage.write("meeting_task_finish_list", taskFinishList); } + + // 触发AI分析,分别请求总结和概览 aiServiceMessage(id, text, prompt, outline); } + ///触发AI分析,分别请求总结和概览 + ///[id] 会议ID + ///[text] 语音转文字结果 + ///[prompt] AI总结提示词 + ///[outline] AI概览提示词 void aiServiceMessage(int id, String text, String prompt, String outline) { int awaitQuantity = 0; String summaryText = ''; @@ -300,6 +398,7 @@ class MeetingTaskService extends GetxService { ).then((value) { awaitQuantity++; summaryText = value; + Logger.info("lxm---大模型asr总结结果$summaryText"); if (awaitQuantity >= 2) { _aiServiceValue(id, summaryText, overviewText); } @@ -315,12 +414,17 @@ class MeetingTaskService extends GetxService { ).then((value) { awaitQuantity++; overviewText = value; + Logger.info("lxm---大模型asr概览结果$overviewText"); if (awaitQuantity >= 2) { _aiServiceValue(id, summaryText, overviewText); } }); } + ///处理AI分析完成后的结果,包括数据库存储和状态更新 + ///[id] 会议ID + ///[summaryText] 总结结果 + ///[overviewText] 概览结果 void _aiServiceValue(int id, String summaryText, String overviewText) { SqfliteApi.editMeeting(id, { 'tasktype': 3, diff --git a/lib/data/services/version_update_service.dart b/lib/data/services/version_update_service.dart index e26ed08eb..482da1069 100644 --- a/lib/data/services/version_update_service.dart +++ b/lib/data/services/version_update_service.dart @@ -211,6 +211,7 @@ class VersionUpdateService extends GetxService { /// 获取当前平台信息 Future getCurrentPlatformInfo() async { if (Platform.isIOS) { + await _getIosDeviceBrand(); return const PlatformInfo( platform: 'ios', storeType: AppStoreType.appStore, @@ -310,6 +311,41 @@ class VersionUpdateService extends GetxService { } } + /// 获取iOS设备品牌信息 + Future _getIosDeviceBrand() async { + try { + final deviceInfo = DeviceInfoPlugin(); + final iosInfo = await deviceInfo.iosInfo; + + // 获取设备型号信息 + final model = + iosInfo.model.toLowerCase(); // 例如: "iPhone", "iPad", "iPod touch" + //print('lxm---model: $model'); + final utsname = iosInfo.utsname; // 包含更详细的设备信息 + //print('lxm---utsname: $utsname'); + final machineId = utsname.machine.toLowerCase(); // 例如: "iPhone14,2" + //print('lxm---machineId: $machineId'); + + // 根据设备型号确定品牌 + if (model.contains('iphone') || machineId.contains('iphone')) { + _deviceBrand = 'iphone'; + } else if (model.contains('ipad') || machineId.contains('ipad')) { + _deviceBrand = 'ipad'; + } else if (model.contains('ipod') || machineId.contains('ipod')) { + _deviceBrand = 'ipod'; + } else if (model.contains('mac') || machineId.contains('mac')) { + _deviceBrand = 'mac'; + } else { + _deviceBrand = 'ios'; // 默认值 + } + + _logger.d('iOS设备品牌: $_deviceBrand (model: $model, machine: $machineId)'); + } catch (e) { + _logger.e('获取iOS设备品牌失败: $e'); + _deviceBrand = 'ios'; // 默认值 + } + } + /// 判断是否需要强制更新 /// 当主版本号(第一位数字)不同时返回true bool _shouldForceUpdate(String current, String latest) { diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 55169ac42..ebae04500 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -1002,6 +1002,7 @@ class AgentController extends GetxController with WidgetsBindingObserver { if (function_name.contains("music")) { Future.delayed(Duration(seconds: 1), () { _qqmusicManager.syncCurrentPlayInfo(isOpenPlay: false); + musicStatus(); }); // } } diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index d24352ca6..90c66e6b8 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -25,6 +25,9 @@ class _AgentViewState extends State with WidgetsBindingObserver { late MessageActionController messageActionController; double _keyboardHeight = 0; + // 👇 新增:用于控制 TextField 焦点 + final FocusNode _textFocusNode = FocusNode(); + @override void initState() { super.initState(); @@ -35,6 +38,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { @override void dispose() { + _textFocusNode.dispose(); WidgetsBinding.instance.removeObserver(this); super.dispose(); } @@ -286,65 +290,102 @@ class _AgentViewState extends State with WidgetsBindingObserver { children: [ // 聊天内容 Expanded( - child: Obx(() { - return Align( - alignment: Alignment.topCenter, - child: ListView.builder( - controller: controller.scrollController, - shrinkWrap: true, - reverse: true, - padding: EdgeInsets.symmetric( - vertical: 16.h, horizontal: 16.w), - itemCount: controller.messages.length + - (controller.hasMoreMessages.value ? 1 : 0), - itemBuilder: (context, index) { - // 如果是最后一项且有更多消息,显示加载指示器 - if (index == controller.messages.length && - controller.hasMoreMessages.value) { - return _buildLoadMoreIndicator(); - } + child: GestureDetector( + onTap: () { + // 点击聊天区域时,收起键盘并切换工具显示状态 + FocusScope.of(context).unfocus(); + if (controller.isPlusOptionsExpanded.value) { + controller.collapsePlusOptions(); + } + }, + child: Obx(() { + return Align( + alignment: Alignment.topCenter, + child: ListView.builder( + controller: controller.scrollController, + shrinkWrap: true, + reverse: true, + padding: EdgeInsets.symmetric( + vertical: 16.h, horizontal: 16.w), + itemCount: controller.messages.length + + (controller.hasMoreMessages.value ? 1 : 0), + itemBuilder: (context, index) { + // 如果是最后一项且有更多消息,显示加载指示器 + if (index == controller.messages.length && + controller.hasMoreMessages.value) { + return _buildLoadMoreIndicator(); + } - final message = controller - .messages[controller.messages.length - 1 - index]; - return index == 0 - ? IntroStepBuilder( - group: 'ai', - order: 4, - getOverlayPosition: - IntroOverlayBuilder.overlayPosition, - overlayBuilder: (params) { - return IntroOverlayBuilder.aiBuilder( - params, - controller.noviceGuideService.aiStepsList[ - controller - .noviceGuideService.aiStep]['end'], - onFinish: () async { - if (controller.noviceGuideService.aiStep < - 5) { - await Future.delayed( - const Duration(milliseconds: 300)); - controller.noviceGuideService.aiStep++; - Intro.of(Get.context!) - .start(group: 'ai', reset: true); - } - }, - onTap: () async { - params.onFinish(); - if (controller.noviceGuideService.aiStep < - 5) { - await Future.delayed( - const Duration(milliseconds: 300)); - controller.noviceGuideService.aiStep++; - Intro.of(Get.context!) - .start(group: 'ai', reset: true); - } - }, - ); - }, - builder: - (BuildContext context, GlobalKey key) => - Padding( - key: key, + final message = controller + .messages[controller.messages.length - 1 - index]; + return index == 0 + ? IntroStepBuilder( + group: 'ai', + order: 4, + getOverlayPosition: + IntroOverlayBuilder.overlayPosition, + overlayBuilder: (params) { + return IntroOverlayBuilder.aiBuilder( + params, + controller.noviceGuideService.aiStepsList[ + controller.noviceGuideService + .aiStep]['end'], + onFinish: () async { + if (controller + .noviceGuideService.aiStep < + 5) { + await Future.delayed(const Duration( + milliseconds: 300)); + controller + .noviceGuideService.aiStep++; + Intro.of(Get.context!) + .start(group: 'ai', reset: true); + } + }, + onTap: () async { + params.onFinish(); + if (controller + .noviceGuideService.aiStep < + 5) { + await Future.delayed(const Duration( + milliseconds: 300)); + controller + .noviceGuideService.aiStep++; + Intro.of(Get.context!) + .start(group: 'ai', reset: true); + } + }, + ); + }, + builder: + (BuildContext context, GlobalKey key) => + Padding( + key: key, + padding: EdgeInsets.only(bottom: 8.h), + child: GestureDetector( + onLongPressStart: + (LongPressStartDetails details) { + messageActionController + .onLongPressMessage(context, + isDarkMode, details, message); + }, + child: MessageBubble( + sessionid: message.sessionid, + isUser: message.isUser, + message: message.text, + timestamp: message.timestamp, + isRecognizing: message.isRecognizing, + hasImage: message.hasImage, + imagePath: message.imagePath, + meta: message.meta, + functionCalls: message.functionCalls, + isToolCallsExpanded: + message.isToolCallsExpanded, + ), + ), + ), + ) + : Padding( padding: EdgeInsets.only(bottom: 8.h), child: GestureDetector( onLongPressStart: @@ -367,35 +408,12 @@ class _AgentViewState extends State with WidgetsBindingObserver { message.isToolCallsExpanded, ), ), - ), - ) - : Padding( - padding: EdgeInsets.only(bottom: 8.h), - child: GestureDetector( - onLongPressStart: - (LongPressStartDetails details) { - messageActionController.onLongPressMessage( - context, isDarkMode, details, message); - }, - child: MessageBubble( - sessionid: message.sessionid, - isUser: message.isUser, - message: message.text, - timestamp: message.timestamp, - isRecognizing: message.isRecognizing, - hasImage: message.hasImage, - imagePath: message.imagePath, - meta: message.meta, - functionCalls: message.functionCalls, - isToolCallsExpanded: - message.isToolCallsExpanded, - ), - ), - ); - }, - ), - ); - }), + ); + }, + ), + ); + }), + ), ), // 输入区域 @@ -553,6 +571,13 @@ class _AgentViewState extends State with WidgetsBindingObserver { return GestureDetector( onTap: () { controller.toggleInputMode(); + // 切换模式时,确保焦点正确 + if (!isTextMode) { + // 切换到语音模式时,请求焦点 + Future.delayed(Duration.zero, () { + _textFocusNode.requestFocus(); + }); + } }, child: Container( width: 44.w, @@ -652,6 +677,7 @@ class _AgentViewState extends State with WidgetsBindingObserver { controller.isImageInputActive.value; return TextField( + focusNode: _textFocusNode, // 绑定焦点节点 controller: controller.textController, maxLines: isImageActive ? 3 diff --git a/lib/modules/agent/views/music_card.dart b/lib/modules/agent/views/music_card.dart index 841b721a2..b60025eef 100644 --- a/lib/modules/agent/views/music_card.dart +++ b/lib/modules/agent/views/music_card.dart @@ -17,7 +17,6 @@ Widget buildMusicItem( 'coverUrl': image, }; - controller.musicStatus(); //刷新状态 return Container( padding: EdgeInsets.all(14.w), decoration: BoxDecoration( diff --git a/lib/modules/login/constants/country_codes.dart b/lib/modules/login/constants/country_codes.dart new file mode 100644 index 000000000..4f7e2202f --- /dev/null +++ b/lib/modules/login/constants/country_codes.dart @@ -0,0 +1,127 @@ +// Country dialing codes list extracted for maintainability and reuse. + +final List> kCountryCodes = [ + {'name': '邮箱', 'code': '', 'flag': '✉️'}, // 保持首位 + {'name': '中国', 'code': '+86', 'flag': '🇨🇳'}, // 保持第二位 + + // 按国家代码数字升序排列(去除+号后排序) + {'name': '美国', 'code': '+1', 'flag': '🇺🇸'}, + {'name': '加拿大', 'code': '+1', 'flag': '🇨🇦'}, + {'name': '俄罗斯', 'code': '+7', 'flag': '🇷🇺'}, + {'name': '英国', 'code': '+44', 'flag': '🇬🇧'}, + {'name': '德国', 'code': '+49', 'flag': '🇩🇪'}, + {'name': '意大利', 'code': '+39', 'flag': '🇮🇹'}, + {'name': '法国', 'code': '+33', 'flag': '🇫🇷'}, + {'name': '西班牙', 'code': '+34', 'flag': '🇪🇸'}, + {'name': '南非', 'code': '+27', 'flag': '🇿🇦'}, + {'name': '沙特阿拉伯', 'code': '+966', 'flag': '🇸🇦'}, + {'name': '土耳其', 'code': '+90', 'flag': '🇹🇷'}, + {'name': '日本', 'code': '+81', 'flag': '🇯🇵'}, + {'name': '韩国', 'code': '+82', 'flag': '🇰🇷'}, + {'name': '新加坡', 'code': '+65', 'flag': '🇸🇬'}, + {'name': '马来西亚', 'code': '+60', 'flag': '🇲🇾'}, + {'name': '泰国', 'code': '+66', 'flag': '🇹🇭'}, + {'name': '越南', 'code': '+84', 'flag': '🇻🇳'}, + {'name': '印度', 'code': '+91', 'flag': '🇮🇳'}, + {'name': '巴基斯坦', 'code': '+92', 'flag': '🇵🇰'}, + {'name': '菲律宾', 'code': '+63', 'flag': '🇵🇭'}, + {'name': '印度尼西亚', 'code': '+62', 'flag': '🇮🇩'}, + {'name': '阿联酋', 'code': '+971', 'flag': '🇦🇪'}, + {'name': '墨西哥', 'code': '+52', 'flag': '🇲🇽'}, + {'name': '巴西', 'code': '+55', 'flag': '🇧🇷'}, + {'name': '阿根廷', 'code': '+54', 'flag': '🇦🇷'}, + {'name': '澳大利亚', 'code': '+61', 'flag': '🇦🇺'}, + {'name': '新西兰', 'code': '+64', 'flag': '🇳🇿'}, + + // 中国地区(保持最后) + {'name': '中国香港', 'code': '+852', 'flag': '🇭🇰'}, + {'name': '中国澳门', 'code': '+853', 'flag': '🇲🇴'}, + {'name': '中国台湾', 'code': '+886', 'flag': '🇹🇼'}, + + // 添加的其他主要国家 + {'name': '埃及', 'code': '+20', 'flag': '🇪🇬'}, + {'name': '希腊', 'code': '+30', 'flag': '🇬🇷'}, + {'name': '荷兰', 'code': '+31', 'flag': '🇳🇱'}, + {'name': '比利时', 'code': '+32', 'flag': '🇧🇪'}, + {'name': '匈牙利', 'code': '+36', 'flag': '🇭🇺'}, + {'name': '罗马尼亚', 'code': '+40', 'flag': '🇷🇴'}, + {'name': '瑞士', 'code': '+41', 'flag': '🇨🇭'}, + {'name': '奥地利', 'code': '+43', 'flag': '🇦🇹'}, + {'name': '丹麦', 'code': '+45', 'flag': '🇩🇰'}, + {'name': '瑞典', 'code': '+46', 'flag': '🇸🇪'}, + {'name': '挪威', 'code': '+47', 'flag': '🇳🇴'}, + {'name': '波兰', 'code': '+48', 'flag': '🇵🇱'}, + {'name': '秘鲁', 'code': '+51', 'flag': '🇵🇪'}, + {'name': '智利', 'code': '+56', 'flag': '🇨🇱'}, + {'name': '哥伦比亚', 'code': '+57', 'flag': '🇨🇴'}, + {'name': '委内瑞拉', 'code': '+58', 'flag': '🇻🇪'}, + {'name': '葡萄牙', 'code': '+351', 'flag': '🇵🇹'}, + {'name': '卢森堡', 'code': '+352', 'flag': '🇱🇺'}, + {'name': '爱尔兰', 'code': '+353', 'flag': '🇮🇪'}, + {'name': '冰岛', 'code': '+354', 'flag': '🇮🇸'}, + {'name': '阿尔巴尼亚', 'code': '+355', 'flag': '🇦🇱'}, + {'name': '马耳他', 'code': '+356', 'flag': '🇲🇹'}, + {'name': '塞浦路斯', 'code': '+357', 'flag': '🇨🇾'}, + {'name': '芬兰', 'code': '+358', 'flag': '🇫🇮'}, + {'name': '保加利亚', 'code': '+359', 'flag': '🇧🇬'}, + {'name': '立陶宛', 'code': '+370', 'flag': '🇱🇹'}, + {'name': '拉脱维亚', 'code': '+371', 'flag': '🇱🇻'}, + {'name': '爱沙尼亚', 'code': '+372', 'flag': '🇪🇪'}, + {'name': '乌克兰', 'code': '+380', 'flag': '🇺🇦'}, + {'name': '塞尔维亚', 'code': '+381', 'flag': '🇷🇸'}, + {'name': '克罗地亚', 'code': '+385', 'flag': '🇭🇷'}, + {'name': '斯洛文尼亚', 'code': '+386', 'flag': '🇸🇮'}, + {'name': '捷克', 'code': '+420', 'flag': '🇨🇿'}, + {'name': '斯洛伐克', 'code': '+421', 'flag': '🇸🇰'}, + {'name': '危地马拉', 'code': '+502', 'flag': '🇬🇹'}, + {'name': '萨尔瓦多', 'code': '+503', 'flag': '🇸🇻'}, + {'name': '洪都拉斯', 'code': '+504', 'flag': '🇭🇳'}, + {'name': '尼加拉瓜', 'code': '+505', 'flag': '🇳🇮'}, + {'name': '哥斯达黎加', 'code': '+506', 'flag': '🇨🇷'}, + {'name': '巴拿马', 'code': '+507', 'flag': '🇵🇦'}, + {'name': '海地', 'code': '+509', 'flag': '🇭🇹'}, + {'name': '玻利维亚', 'code': '+591', 'flag': '🇧🇴'}, + {'name': '厄瓜多尔', 'code': '+593', 'flag': '🇪🇨'}, + {'name': '巴拉圭', 'code': '+595', 'flag': '🇵🇾'}, + {'name': '乌拉圭', 'code': '+598', 'flag': '🇺🇾'}, + {'name': '摩洛哥', 'code': '+212', 'flag': '🇲🇦'}, + {'name': '阿尔及利亚', 'code': '+213', 'flag': '🇩🇿'}, + {'name': '突尼斯', 'code': '+216', 'flag': '🇹🇳'}, + {'name': '尼日利亚', 'code': '+234', 'flag': '🇳🇬'}, + {'name': '加纳', 'code': '+233', 'flag': '🇬🇭'}, + {'name': '埃塞俄比亚', 'code': '+251', 'flag': '🇪🇹'}, + {'name': '肯尼亚', 'code': '+254', 'flag': '🇰🇪'}, + {'name': '坦桑尼亚', 'code': '+255', 'flag': '🇹🇿'}, + {'name': '乌干达', 'code': '+256', 'flag': '🇺🇬'}, + {'name': '孟加拉国', 'code': '+880', 'flag': '🇧🇩'}, + {'name': '以色列', 'code': '+972', 'flag': '🇮🇱'}, + {'name': '黎巴嫩', 'code': '+961', 'flag': '🇱🇧'}, + {'name': '约旦', 'code': '+962', 'flag': '🇯🇴'}, + {'name': '伊拉克', 'code': '+964', 'flag': '🇮🇶'}, + {'name': '科威特', 'code': '+965', 'flag': '🇰🇼'}, + {'name': '巴林', 'code': '+973', 'flag': '🇧🇭'}, + {'name': '卡塔尔', 'code': '+974', 'flag': '🇶🇦'}, + {'name': '阿曼', 'code': '+968', 'flag': '🇴🇲'}, + {'name': '阿富汗', 'code': '+93', 'flag': '🇦🇫'}, + {'name': '斯里兰卡', 'code': '+94', 'flag': '🇱🇰'}, + {'name': '缅甸', 'code': '+95', 'flag': '🇲🇲'}, + {'name': '尼泊尔', 'code': '+977', 'flag': '🇳🇵'}, + {'name': '蒙古', 'code': '+976', 'flag': '🇲🇳'}, + {'name': '柬埔寨', 'code': '+855', 'flag': '🇰🇭'}, + {'name': '老挝', 'code': '+856', 'flag': '🇱🇦'}, + {'name': '文莱', 'code': '+673', 'flag': '🇧🇳'}, + {'name': '东帝汶', 'code': '+670', 'flag': '🇹🇱'}, + {'name': '斐济', 'code': '+679', 'flag': '🇫🇯'}, + {'name': '所罗门群岛', 'code': '+677', 'flag': '🇸🇧'}, + {'name': '刚果民主共和国', 'code': '+243', 'flag': '🇨🇩'}, + {'name': '安哥拉', 'code': '+244', 'flag': '🇦🇴'}, + {'name': '苏丹', 'code': '+249', 'flag': '🇸🇩'}, + {'name': '马尔代夫', 'code': '+960', 'flag': '🇲🇻'}, + {'name': '不丹', 'code': '+975', 'flag': '🇧🇹'}, + {'name': '摩纳哥', 'code': '+377', 'flag': '🇲🇨'}, + {'name': '圣马力诺', 'code': '+378', 'flag': '🇸🇲'}, + {'name': '牙买加', 'code': '+1-876', 'flag': '🇯🇲'}, + {'name': '巴哈马', 'code': '+1-242', 'flag': '🇧🇸'}, + {'name': '巴布亚新几内亚', 'code': '+675', 'flag': '🇵🇬'}, + {'name': '萨摩亚', 'code': '+685', 'flag': '🇼🇸'}, +]; diff --git a/lib/modules/login/controllers/login_controller.dart b/lib/modules/login/controllers/login_controller.dart index 066922dbf..1ba740e8e 100644 --- a/lib/modules/login/controllers/login_controller.dart +++ b/lib/modules/login/controllers/login_controller.dart @@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart'; import 'package:logger/web.dart'; import 'package:get_storage/get_storage.dart'; import 'package:device_info_plus/device_info_plus.dart'; +import 'package:crypto/crypto.dart'; // 用于生成设备指纹 import 'dart:convert'; import '../../../data/services/db/sqflite_api.dart'; import 'facebookLogin.dart'; @@ -21,6 +22,7 @@ import '../../../../data/models/user_Info.dart'; import '../../../data/models/channel_app_model.dart'; import '../../../data/services/version_update_service.dart'; import '../../../modules/common/widgets/privacy_policy_dialog.dart'; +import '../constants/country_codes.dart'; enum LoginViewState { welcome, // 欢迎页面 @@ -42,7 +44,7 @@ class LoginController extends GetxController { UserChannel _userChannel = UserChannel.OFFICIAL; // 默认渠道 final isChinaMainland = false.obs; //是否中国大陆地区 - final _hasAcceptedPrivacy = false.obs; + final _hasAcceptedPrivacy = false.obs; //是否同意隐私政策 bool get hasAcceptedPrivacy => _hasAcceptedPrivacy.value; set hasAcceptedPrivacy(bool value) => _hasAcceptedPrivacy.value = value; @@ -53,131 +55,7 @@ class LoginController extends GetxController { String get selectedCountryCode => _selectedCountryCode.value; String get selectedCountryFlag => _selectedCountryFlag.value; // 预设国家编码列表 - final List> countryCodes = [ - {'name': '邮箱', 'code': '', 'flag': '✉️'}, // 保持首位 - {'name': '中国', 'code': '+86', 'flag': '🇨🇳'}, // 保持第二位 - - // 按国家代码数字升序排列(去除+号后排序) - {'name': '美国', 'code': '+1', 'flag': '🇺🇸'}, - {'name': '加拿大', 'code': '+1', 'flag': '🇨🇦'}, - {'name': '俄罗斯', 'code': '+7', 'flag': '🇷🇺'}, - {'name': '英国', 'code': '+44', 'flag': '🇬🇧'}, - {'name': '德国', 'code': '+49', 'flag': '🇩🇪'}, - {'name': '意大利', 'code': '+39', 'flag': '🇮🇹'}, - {'name': '法国', 'code': '+33', 'flag': '🇫🇷'}, - {'name': '西班牙', 'code': '+34', 'flag': '🇪🇸'}, - {'name': '南非', 'code': '+27', 'flag': '🇿🇦'}, - {'name': '沙特阿拉伯', 'code': '+966', 'flag': '🇸🇦'}, - {'name': '土耳其', 'code': '+90', 'flag': '🇹🇷'}, - {'name': '日本', 'code': '+81', 'flag': '🇯🇵'}, - {'name': '韩国', 'code': '+82', 'flag': '🇰🇷'}, - {'name': '新加坡', 'code': '+65', 'flag': '🇸🇬'}, - {'name': '马来西亚', 'code': '+60', 'flag': '🇲🇾'}, - {'name': '泰国', 'code': '+66', 'flag': '🇹🇭'}, - {'name': '越南', 'code': '+84', 'flag': '🇻🇳'}, - {'name': '印度', 'code': '+91', 'flag': '🇮🇳'}, - {'name': '巴基斯坦', 'code': '+92', 'flag': '🇵🇰'}, - {'name': '菲律宾', 'code': '+63', 'flag': '🇵🇭'}, - {'name': '印度尼西亚', 'code': '+62', 'flag': '🇮🇩'}, - {'name': '阿联酋', 'code': '+971', 'flag': '🇦🇪'}, - {'name': '墨西哥', 'code': '+52', 'flag': '🇲🇽'}, - {'name': '巴西', 'code': '+55', 'flag': '🇧🇷'}, - {'name': '阿根廷', 'code': '+54', 'flag': '🇦🇷'}, - {'name': '澳大利亚', 'code': '+61', 'flag': '🇦🇺'}, - {'name': '新西兰', 'code': '+64', 'flag': '🇳🇿'}, - - // 中国地区(保持最后) - {'name': '中国香港', 'code': '+852', 'flag': '🇭🇰'}, - {'name': '中国澳门', 'code': '+853', 'flag': '🇲🇴'}, - {'name': '中国台湾', 'code': '+886', 'flag': '🇹🇼'}, - - // 添加的其他主要国家 - {'name': '埃及', 'code': '+20', 'flag': '🇪🇬'}, - {'name': '希腊', 'code': '+30', 'flag': '🇬🇷'}, - {'name': '荷兰', 'code': '+31', 'flag': '🇳🇱'}, - {'name': '比利时', 'code': '+32', 'flag': '🇧🇪'}, - {'name': '匈牙利', 'code': '+36', 'flag': '🇭🇺'}, - {'name': '罗马尼亚', 'code': '+40', 'flag': '🇷🇴'}, - {'name': '瑞士', 'code': '+41', 'flag': '🇨🇭'}, - {'name': '奥地利', 'code': '+43', 'flag': '🇦🇹'}, - {'name': '丹麦', 'code': '+45', 'flag': '🇩🇰'}, - {'name': '瑞典', 'code': '+46', 'flag': '🇸🇪'}, - {'name': '挪威', 'code': '+47', 'flag': '🇳🇴'}, - {'name': '波兰', 'code': '+48', 'flag': '🇵🇱'}, - {'name': '秘鲁', 'code': '+51', 'flag': '🇵🇪'}, - {'name': '智利', 'code': '+56', 'flag': '🇨🇱'}, - {'name': '哥伦比亚', 'code': '+57', 'flag': '🇨🇴'}, - {'name': '委内瑞拉', 'code': '+58', 'flag': '🇻🇪'}, - {'name': '葡萄牙', 'code': '+351', 'flag': '🇵🇹'}, - {'name': '卢森堡', 'code': '+352', 'flag': '🇱🇺'}, - {'name': '爱尔兰', 'code': '+353', 'flag': '🇮🇪'}, - {'name': '冰岛', 'code': '+354', 'flag': '🇮🇸'}, - {'name': '阿尔巴尼亚', 'code': '+355', 'flag': '🇦🇱'}, - {'name': '马耳他', 'code': '+356', 'flag': '🇲🇹'}, - {'name': '塞浦路斯', 'code': '+357', 'flag': '🇨🇾'}, - {'name': '芬兰', 'code': '+358', 'flag': '🇫🇮'}, - {'name': '保加利亚', 'code': '+359', 'flag': '🇧🇬'}, - {'name': '立陶宛', 'code': '+370', 'flag': '🇱🇹'}, - {'name': '拉脱维亚', 'code': '+371', 'flag': '🇱🇻'}, - {'name': '爱沙尼亚', 'code': '+372', 'flag': '🇪🇪'}, - {'name': '乌克兰', 'code': '+380', 'flag': '🇺🇦'}, - {'name': '塞尔维亚', 'code': '+381', 'flag': '🇷🇸'}, - {'name': '克罗地亚', 'code': '+385', 'flag': '🇭🇷'}, - {'name': '斯洛文尼亚', 'code': '+386', 'flag': '🇸🇮'}, - {'name': '捷克', 'code': '+420', 'flag': '🇨🇿'}, - {'name': '斯洛伐克', 'code': '+421', 'flag': '🇸🇰'}, - {'name': '危地马拉', 'code': '+502', 'flag': '🇬🇹'}, - {'name': '萨尔瓦多', 'code': '+503', 'flag': '🇸🇻'}, - {'name': '洪都拉斯', 'code': '+504', 'flag': '🇭🇳'}, - {'name': '尼加拉瓜', 'code': '+505', 'flag': '🇳🇮'}, - {'name': '哥斯达黎加', 'code': '+506', 'flag': '🇨🇷'}, - {'name': '巴拿马', 'code': '+507', 'flag': '🇵🇦'}, - {'name': '海地', 'code': '+509', 'flag': '🇭🇹'}, - {'name': '玻利维亚', 'code': '+591', 'flag': '🇧🇴'}, - {'name': '厄瓜多尔', 'code': '+593', 'flag': '🇪🇨'}, - {'name': '巴拉圭', 'code': '+595', 'flag': '🇵🇾'}, - {'name': '乌拉圭', 'code': '+598', 'flag': '🇺🇾'}, - {'name': '摩洛哥', 'code': '+212', 'flag': '🇲🇦'}, - {'name': '阿尔及利亚', 'code': '+213', 'flag': '🇩🇿'}, - {'name': '突尼斯', 'code': '+216', 'flag': '🇹🇳'}, - {'name': '尼日利亚', 'code': '+234', 'flag': '🇳🇬'}, - {'name': '加纳', 'code': '+233', 'flag': '🇬🇭'}, - {'name': '埃塞俄比亚', 'code': '+251', 'flag': '🇪🇹'}, - {'name': '肯尼亚', 'code': '+254', 'flag': '🇰🇪'}, - {'name': '坦桑尼亚', 'code': '+255', 'flag': '🇹🇿'}, - {'name': '乌干达', 'code': '+256', 'flag': '🇺🇬'}, - {'name': '孟加拉国', 'code': '+880', 'flag': '🇧🇩'}, - {'name': '以色列', 'code': '+972', 'flag': '🇮🇱'}, - {'name': '黎巴嫩', 'code': '+961', 'flag': '🇱🇧'}, - {'name': '约旦', 'code': '+962', 'flag': '🇯🇴'}, - {'name': '伊拉克', 'code': '+964', 'flag': '🇮🇶'}, - {'name': '科威特', 'code': '+965', 'flag': '🇰🇼'}, - {'name': '巴林', 'code': '+973', 'flag': '🇧🇭'}, - {'name': '卡塔尔', 'code': '+974', 'flag': '🇶🇦'}, - {'name': '阿曼', 'code': '+968', 'flag': '🇴🇲'}, - {'name': '阿富汗', 'code': '+93', 'flag': '🇦🇫'}, - {'name': '斯里兰卡', 'code': '+94', 'flag': '🇱🇰'}, - {'name': '缅甸', 'code': '+95', 'flag': '🇲🇲'}, - {'name': '尼泊尔', 'code': '+977', 'flag': '🇳🇵'}, - {'name': '蒙古', 'code': '+976', 'flag': '🇲🇳'}, - {'name': '柬埔寨', 'code': '+855', 'flag': '🇰🇭'}, - {'name': '老挝', 'code': '+856', 'flag': '🇱🇦'}, - {'name': '文莱', 'code': '+673', 'flag': '🇧🇳'}, - {'name': '东帝汶', 'code': '+670', 'flag': '🇹🇱'}, - {'name': '斐济', 'code': '+679', 'flag': '🇫🇯'}, - {'name': '所罗门群岛', 'code': '+677', 'flag': '🇸🇧'}, - {'name': '刚果民主共和国', 'code': '+243', 'flag': '🇨🇩'}, - {'name': '安哥拉', 'code': '+244', 'flag': '🇦🇴'}, - {'name': '苏丹', 'code': '+249', 'flag': '🇸🇩'}, - {'name': '马尔代夫', 'code': '+960', 'flag': '🇲🇻'}, - {'name': '不丹', 'code': '+975', 'flag': '🇧🇹'}, - {'name': '摩纳哥', 'code': '+377', 'flag': '🇲🇨'}, - {'name': '圣马力诺', 'code': '+378', 'flag': '🇸🇲'}, - {'name': '牙买加', 'code': '+1-876', 'flag': '🇯🇲'}, - {'name': '巴哈马', 'code': '+1-242', 'flag': '🇧🇸'}, - {'name': '巴布亚新几内亚', 'code': '+675', 'flag': '🇵🇬'}, - {'name': '萨摩亚', 'code': '+685', 'flag': '🇼🇸'}, - ]; + final List> countryCodes = kCountryCodes; // 初始化默认选择的国家代码 void _initDefaultCountryCode() { @@ -236,7 +114,7 @@ class LoginController extends GetxController { _verificationCodeController = TextEditingController(); isChinaMainland.value = - Get.deviceLocale?.countryCode?.toUpperCase() == 'CN'; + Get.deviceLocale?.countryCode?.toUpperCase() == 'CN'; //是否中国大陆地区 // 初始化默认国家代码 _initDefaultCountryCode(); @@ -304,9 +182,27 @@ class LoginController extends GetxController { _deviceId = androidInfo.id; } else if (GetPlatform.isIOS) { IosDeviceInfo iosInfo = await deviceInfo.iosInfo; - _deviceId = iosInfo.identifierForVendor ?? 'unknown'; + //print('lxm---iosInfo: $iosInfo.toString()'); + final utsname = iosInfo.utsname; // 包含更详细的设备信息 + //print('lxm---utsname: $utsname'); + final machineId = utsname.machine.toLowerCase(); // 例如: "iPhone14,2" + Logger().d('lxm---machineId: $machineId'); + final systemVersion = utsname.release; // 例如: "16.4.1" + Logger().d('lxm---systemVersion: $systemVersion'); + + try { + // 组合 machine 和 systemVersion 生成指纹 + final fingerprint = '$machineId|$systemVersion'; + final hashedFingerprint = + md5.convert(utf8.encode(fingerprint)).toString(); + Logger() + .d('生成设备指纹: $hashedFingerprint (基于 $machineId 和 $systemVersion)'); + _deviceId = hashedFingerprint; + } catch (e) { + Logger().e('生成设备指纹失败: $e'); + _deviceId = iosInfo.identifierForVendor ?? 'unknown'; + } } - Logger().d('deviceId: $_deviceId'); } catch (e) { Logger().e('获取设备信息失败: $e'); _deviceId = 'unknown'; diff --git a/lib/modules/meeting/controllers/meeting_details_controller.dart b/lib/modules/meeting/controllers/meeting_details_controller.dart index 5e6500734..aaf4eb6f7 100644 --- a/lib/modules/meeting/controllers/meeting_details_controller.dart +++ b/lib/modules/meeting/controllers/meeting_details_controller.dart @@ -69,6 +69,10 @@ class MeetingDetailsController extends GetxController RxBool isDownload = false.obs; RxDouble downloadProgress = 0.0.obs; + List categoryTemplateList = []; + Map editTemplate = {}; + int deleteTemplateId = 0; + @override void onInit() { super.onInit(); diff --git a/lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart index 05eceaf72..e929fdfcf 100644 --- a/lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart +++ b/lib/modules/meeting/views/bottomSheet/generate_bottom_sheet.dart @@ -46,7 +46,6 @@ class _GenerateBottomSheetState extends State { 'auto_fix_high': Icons.auto_fix_high, }; - List _categoryTemplateList = []; List _originalTemplate = []; Map _templateData = {}; @@ -60,10 +59,13 @@ class _GenerateBottomSheetState extends State { } void _getTemplate() async { - _categoryTemplateList = await SqfliteApi.getMeetingCategoryTemplateList(); + _controller.categoryTemplateList = + await SqfliteApi.getMeetingCategoryTemplateList(); + _controller.editTemplate = {}; + _controller.deleteTemplateId = 0; _lastTemplateData = _storage.read("meeting_last_template_Data") ?? {}; _originalTemplate = _storage.read("meeting_template_list") ?? - _categoryTemplateList[0]['template']; + _controller.categoryTemplateList[0]['template']; if (_lastTemplateData.isNotEmpty) { _templateData = _lastTemplateData; } else { @@ -162,7 +164,6 @@ class _GenerateBottomSheetState extends State { () => TemplateView( _templateData, _lastTemplateData['id'] ?? 0, - _categoryTemplateList, ), preventDuplicates: false, ); @@ -182,6 +183,34 @@ class _GenerateBottomSheetState extends State { _templateList = list; }); } + if (_controller.editTemplate.isNotEmpty) { + Map? editMap = _templateList.firstWhere( + (element) => + element['id'] == _controller.editTemplate['id'], + orElse: () => null); + if (editMap != null) { + editMap['name'] = _controller.editTemplate['name']; + editMap['desc'] = _controller.editTemplate['desc']; + editMap['prompt'] = _controller.editTemplate['prompt']; + } + if (_controller.editTemplate['id'] == _templateData['id']) { + _templateData['name'] = _controller.editTemplate['name']; + _templateData['desc'] = _controller.editTemplate['desc']; + _templateData['prompt'] = + _controller.editTemplate['prompt']; + } + setState(() {}); + } + if (_controller.deleteTemplateId != 0) { + _templateList.removeWhere((element) => + element['id'] == _controller.deleteTemplateId); + if (_controller.deleteTemplateId == _templateData['id']) { + _templateData = _templateList[0]; + } + setState(() {}); + _storage.write("meeting_last_template_Data", {}); + _storage.write("meeting_template_list", _templateList); + } }, child: Row( children: [ diff --git a/lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart b/lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart index 7f594cdc6..6700edd92 100644 --- a/lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart +++ b/lib/modules/meeting/views/bottomSheet/navigation_bar_bottom_sheet.dart @@ -5,6 +5,7 @@ import 'package:get/get.dart'; import '../../../../routes/app_routes.dart'; import '../../controllers/meeting_home_controller.dart'; +///会议底部导航栏抽屉,提供会议列表、会议详情、会议设置 class NavigationBarBottomSheet extends GetView { const NavigationBarBottomSheet({super.key}); diff --git a/lib/modules/meeting/views/meeting_details_view.dart b/lib/modules/meeting/views/meeting_details_view.dart index 15444981a..cd21f85a5 100644 --- a/lib/modules/meeting/views/meeting_details_view.dart +++ b/lib/modules/meeting/views/meeting_details_view.dart @@ -17,6 +17,7 @@ import 'bottomSheet/share_bottom_sheet.dart'; import 'tabs/speech_tab.dart'; import 'tabs/summary_tab.dart'; +///会议音频详情页面,提供会议音频播放、会议音频分享、会议音频更多操作 class MeetingDetailsView extends GetView { const MeetingDetailsView({super.key}); @@ -40,10 +41,9 @@ class MeetingDetailsView extends GetView { preferredSize: const Size.fromHeight(kToolbarHeight), child: Obx( () => AppBar( - backgroundColor: - isDarkMode - ? (controller.isTop.value ? Colors.black : Colors.grey[900]) - : (controller.isTop.value ? Colors.grey[50] : Colors.grey[200]), + backgroundColor: isDarkMode + ? (controller.isTop.value ? Colors.black : Colors.grey[900]) + : (controller.isTop.value ? Colors.grey[50] : Colors.grey[200]), surfaceTintColor: Colors.transparent, elevation: 0.5, leading: controller.isSpeakerText.value @@ -202,7 +202,8 @@ class MeetingDetailsView extends GetView { Get.bottomSheet( const ShareBottomSheet(), isScrollControlled: true, - backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100], + backgroundColor: + isDarkMode ? Colors.grey[800] : Colors.grey[100], shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(12.r), @@ -234,7 +235,8 @@ class MeetingDetailsView extends GetView { Get.bottomSheet( const MoreBottomSheet(), isScrollControlled: true, - backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100], + backgroundColor: + isDarkMode ? Colors.grey[800] : Colors.grey[100], shape: RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(12.r), @@ -301,10 +303,14 @@ class MeetingDetailsView extends GetView { child: Obx( () => Row( children: [ - _iconItem(Icons.summarize, 'overview'.tr, 0), // 对应中文:概览 - _iconItem(Icons.message, 'transcription'.tr, 1), // 对应中文:转写 - _iconItem(Icons.article, 'summary'.tr, 2), // 对应中文:总结 - _iconItem(Icons.lan, 'mindMap'.tr, 3), // 对应中文:思维导图 + _iconItem( + Icons.summarize, 'overview'.tr, 0), // 对应中文:概览 + _iconItem(Icons.message, 'transcription'.tr, + 1), // 对应中文:转写 + _iconItem( + Icons.article, 'summary'.tr, 2), // 对应中文:总结 + _iconItem( + Icons.lan, 'mindMap'.tr, 3), // 对应中文:思维导图 ], ), ), diff --git a/lib/modules/meeting/views/meeting_home_view.dart b/lib/modules/meeting/views/meeting_home_view.dart index f1c9ed0f8..81cd3b22b 100644 --- a/lib/modules/meeting/views/meeting_home_view.dart +++ b/lib/modules/meeting/views/meeting_home_view.dart @@ -11,6 +11,7 @@ import '../model/meeting_model.dart'; import '../utils/meeting_ui_utils.dart'; import 'bottomSheet/edit_title_bottom_sheet.dart'; +///会议主页面,提供会议列表、会议详情、会议设置 class MeetingHomeView extends StatefulWidget { const MeetingHomeView({super.key}); diff --git a/lib/modules/meeting/views/meeting_view.dart b/lib/modules/meeting/views/meeting_view.dart index a2aa30687..27cbdd32f 100644 --- a/lib/modules/meeting/views/meeting_view.dart +++ b/lib/modules/meeting/views/meeting_view.dart @@ -9,6 +9,7 @@ import '../utils/meeting_ui_utils.dart'; import 'bottomSheet/filtrate_drawer.dart'; import 'bottomSheet/navigation_bar_bottom_sheet.dart'; +///meeting模块主容器页面,提供底部导航栏、浮动按钮、侧板抽屉 class MeetingView extends GetView { const MeetingView({super.key}); diff --git a/lib/modules/meeting/views/template/template_all_view.dart b/lib/modules/meeting/views/template/template_all_view.dart index 7100e20b6..7da711ba8 100644 --- a/lib/modules/meeting/views/template/template_all_view.dart +++ b/lib/modules/meeting/views/template/template_all_view.dart @@ -3,6 +3,8 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import '../../../../data/services/db/sqflite_api.dart'; +import '../../controllers/meeting_details_controller.dart'; +import 'template_details_view.dart'; class TemplateAllView extends StatefulWidget { final String title; @@ -19,6 +21,8 @@ class TemplateAllView extends StatefulWidget { } class _TemplateAllViewState extends State { + final _controller = Get.find(); + final Map _icons = { 'auto_stories': Icons.auto_stories, 'auto_graph': Icons.auto_graph, @@ -47,8 +51,9 @@ class _TemplateAllViewState extends State { _getTemplate(); } - void _getTemplate() async { - List list = await SqfliteApi.getMeetingTemplateList(widget.categoryid); + Future _getTemplate() async { + List list = await SqfliteApi.getMeetingTemplateList(widget.categoryid, + isCustom: widget.title == '自定义'); List templateList = []; for (var element in list) { Map item = { @@ -245,6 +250,39 @@ class _TemplateAllViewState extends State { ), ), ), + Positioned( + top: 0, + right: 0, + child: GestureDetector( + onTap: () async { + await Get.to(() => TemplateDetailsView(template, + isEdit: widget.title == '自定义')); + if (_controller.editTemplate.isNotEmpty) { + await _getTemplate(); + if (_controller.editTemplate['id'] == _templateData['id']) { + _templateData['name'] = _controller.editTemplate['name']; + _templateData['desc'] = _controller.editTemplate['desc']; + _templateData['prompt'] = + _controller.editTemplate['prompt']; + } + } + if (_controller.deleteTemplateId != 0) { + await _getTemplate(); + if (_controller.deleteTemplateId == _templateData['id']) { + _templateData = {}; + } + } + }, + child: Padding( + padding: EdgeInsets.all(10.w), + child: Icon( + Icons.fullscreen, + color: Colors.grey[600], + size: 26.w, + ), + ), + ), + ), if (widget.lastTemplateId == template['id']) Positioned( top: 0, diff --git a/lib/modules/meeting/views/template/template_details_view.dart b/lib/modules/meeting/views/template/template_details_view.dart index 4150e1c70..fc517e280 100644 --- a/lib/modules/meeting/views/template/template_details_view.dart +++ b/lib/modules/meeting/views/template/template_details_view.dart @@ -3,10 +3,31 @@ import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; -class TemplateDetailsView extends StatelessWidget { +import '../../../../data/services/db/sqflite_api.dart'; +import '../../../../data/services/network/api.dart'; +import '../../controllers/meeting_details_controller.dart'; +import 'template_edit_view.dart'; + +class TemplateDetailsView extends StatefulWidget { final Map data; + final bool isEdit; + + const TemplateDetailsView(this.data, {super.key, this.isEdit = false}); + + @override + State createState() => _TemplateDetailsViewState(); +} + +class _TemplateDetailsViewState extends State { + final _controller = Get.find(); + + Map _templateData = {}; - const TemplateDetailsView(this.data, {super.key}); + @override + void initState() { + super.initState(); + _templateData = widget.data; + } @override Widget build(BuildContext context) { @@ -55,7 +76,7 @@ class TemplateDetailsView extends StatelessWidget { Padding( padding: EdgeInsets.only(top: 20.w), child: Text( - data['name'], + _templateData['name'], style: TextStyle( fontSize: 20.sp, fontWeight: FontWeight.bold, @@ -75,7 +96,7 @@ class TemplateDetailsView extends StatelessWidget { Wrap( spacing: 5.w, children: List.generate( - data['tag'].length, + _templateData['tag'].length, (int idx) { return Container( padding: @@ -85,7 +106,7 @@ class TemplateDetailsView extends StatelessWidget { borderRadius: BorderRadius.circular(4.r), ), child: Text( - data['tag'][idx], + _templateData['tag'][idx], style: TextStyle( fontSize: 10.sp, color: Colors.grey[600], @@ -106,7 +127,7 @@ class TemplateDetailsView extends StatelessWidget { ), ), Text( - data['desc'], + _templateData['desc'], style: TextStyle( fontSize: 14.sp, color: Colors.grey[600], @@ -123,61 +144,130 @@ class TemplateDetailsView extends StatelessWidget { ), ), MarkdownBody( - data: data['prompt'], + data: _templateData['prompt'], ), ], ), ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w), - child: Row( - children: [ - Expanded( - child: GestureDetector( - onTap: () {}, - child: Container( - height: 48.w, - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12.r), - border: Border.all( - color: Colors.black, + if (widget.isEdit) + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w), + child: Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () async { + var result = await Get.to( + () => TemplateEditView(data: _templateData)); + if (result != null) { + setState(() { + _templateData['name'] = result['name']; + _templateData['desc'] = result['desc']; + _templateData['prompt'] = result['prompt']; + }); + } + }, + child: Container( + height: 48.w, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.r), + border: Border.all( + color: Colors.black, + ), ), - ), - child: Text( - '修改', - style: TextStyle( - fontSize: 16.sp, - color: Colors.black, + child: Text( + '修改', + style: TextStyle( + fontSize: 16.sp, + color: Colors.black, + ), ), ), ), ), - ), - 15.horizontalSpace, - Expanded( - child: GestureDetector( - onTap: () {}, - child: Container( - height: 48.w, - alignment: Alignment.center, - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(12.r), - ), - child: Text( - '删除', - style: TextStyle( - fontSize: 16.sp, - color: Colors.white, + 15.horizontalSpace, + Expanded( + child: GestureDetector( + onTap: () { + Get.dialog( + AlertDialog( + backgroundColor: + isDarkMode ? Colors.grey[900] : Colors.white, + title: Text( + '删除模版', + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + ), + ), + content: Text( + '确定要删除当前模版吗?', + style: TextStyle( + color: isDarkMode + ? Colors.grey[400] + : Colors.grey[600], + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text( + 'cancel'.tr, + style: TextStyle( + color: isDarkMode + ? Colors.grey[400] + : Colors.grey[600], + ), + ), + ), + TextButton( + onPressed: () async { + Get.back(); + var response = await Api.delEchomeetTemplates({ + 'ids': [_templateData['id']], + }); + if (response != null) { + await SqfliteApi.deleteMeetingTemplate( + _templateData['id']); + _controller.categoryTemplateList = + await SqfliteApi + .getMeetingCategoryTemplateList(); + _controller.deleteTemplateId = + _templateData['id']; + Get.back(); + } + }, + child: Text( + 'confirm'.tr, + style: const TextStyle( + color: Colors.red, + ), + ), + ), + ], + ), + ); + }, + child: Container( + height: 48.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(12.r), + ), + child: Text( + '删除', + style: TextStyle( + fontSize: 16.sp, + color: Colors.white, + ), ), ), ), ), - ), - ], + ], + ), ), - ), ], ); } diff --git a/lib/modules/meeting/views/template/template_edit_view.dart b/lib/modules/meeting/views/template/template_edit_view.dart index 79426d117..6c69d3596 100644 --- a/lib/modules/meeting/views/template/template_edit_view.dart +++ b/lib/modules/meeting/views/template/template_edit_view.dart @@ -2,18 +2,25 @@ import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; import '../../../../data/services/db/sqflite_api.dart'; import '../../../../data/services/network/api.dart'; +import '../../controllers/meeting_details_controller.dart'; class TemplateEditView extends StatefulWidget { - const TemplateEditView({super.key}); + final Map? data; + final int? categoryid; + + const TemplateEditView({super.key, this.data, this.categoryid}); @override State createState() => _TemplateEditViewState(); } class _TemplateEditViewState extends State { + final _controller = Get.find(); + final TextEditingController _nameController = TextEditingController(); final TextEditingController _descController = TextEditingController(); final TextEditingController _promptController = TextEditingController(); @@ -23,6 +30,11 @@ class _TemplateEditViewState extends State { @override void initState() { super.initState(); + if (widget.data != null) { + _nameController.text = widget.data?['name']; + _descController.text = widget.data?['desc']; + _promptController.text = widget.data?['prompt']; + } } @override @@ -61,7 +73,7 @@ class _TemplateEditViewState extends State { onPressed: () => Get.back(), ), title: Text( - '创建模版', + widget.data != null ? '编辑模版' : '创建模版', style: TextStyle( fontSize: 17.sp, fontWeight: FontWeight.w600, @@ -204,7 +216,7 @@ class _TemplateEditViewState extends State { void _submit() async { EasyLoading.show(); Map data = { - "template": { + "template": { 'ttype': '自定义', 'tags': '', 'icon': 'auto_fix_high', @@ -214,11 +226,71 @@ class _TemplateEditViewState extends State { 'template': _promptController.text, }, }; - var response = await Api.addEchomeetTemplates(data); - if (response != null) { - print('----$response--------'); - // await SqfliteApi.insertMeetingTemplate(data['template']); + if (widget.data != null) { + data['template']['id'] = widget.data?['id']; + var response = await Api.updateEchomeetTemplates(data); + if (response != null) { + await SqfliteApi.editMeetingTemplate(widget.data?['id'], { + 'name': _nameController.text, + 'desc': _descController.text, + 'prompt': _promptController.text, + }); + _controller.editTemplate = { + 'id': widget.data?['id'], + 'name': _nameController.text, + 'desc': _descController.text, + 'prompt': _promptController.text, + }; + final GetStorage storage = GetStorage(); + Map lastTemplateData = storage.read("meeting_last_template_Data") ?? {}; + List originalTemplate = storage.read("meeting_template_list") ?? []; + if (lastTemplateData.isNotEmpty && + lastTemplateData['id'] == widget.data?['id']) { + lastTemplateData['name'] = _nameController.text; + lastTemplateData['desc'] = _descController.text; + lastTemplateData['prompt'] = _promptController.text; + storage.write("meeting_last_template_Data", lastTemplateData); + } + if (originalTemplate.isNotEmpty) { + Map? originalMap = originalTemplate.firstWhere( + (element) => element['id'] == widget.data?['id'], + orElse: () => null); + if (originalMap != null) { + originalMap['name'] = _nameController.text; + originalMap['desc'] = _descController.text; + originalMap['prompt'] = _promptController.text; + storage.write("meeting_template_list", originalTemplate); + } + } + } + } else { + var response = await Api.addEchomeetTemplates(data); + if (response != null) { + await SqfliteApi.insertMeetingTemplate({ + "id": response['template']['id'], + 'categoryid': widget.categoryid, + 'name': _nameController.text, + 'icon': 'auto_fix_high', + 'tag': '', + 'desc': _descController.text, + 'outline': '您将扮演一名会议事件归纳专家,能准确抓住会议中事件的关键点,并每个事件只需一句话精简总结。', + 'prompt': _promptController.text, + }); + _controller.editTemplate = { + 'id': response['template']['id'], + 'name': _nameController.text, + 'desc': _descController.text, + 'prompt': _promptController.text, + }; + } } + _controller.categoryTemplateList = + await SqfliteApi.getMeetingCategoryTemplateList(); + Get.back(result: { + 'name': _nameController.text, + 'desc': _descController.text, + 'prompt': _promptController.text, + }); EasyLoading.dismiss(); } } diff --git a/lib/modules/meeting/views/template/template_view.dart b/lib/modules/meeting/views/template/template_view.dart index 2a476f7e5..84fd2fb2e 100644 --- a/lib/modules/meeting/views/template/template_view.dart +++ b/lib/modules/meeting/views/template/template_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import '../../controllers/meeting_details_controller.dart'; import 'template_all_view.dart'; import 'template_details_view.dart'; import 'template_edit_view.dart'; @@ -9,16 +10,16 @@ import 'template_edit_view.dart'; class TemplateView extends StatefulWidget { final Map templateData; final int lastTemplateId; - final List categoryList; - const TemplateView(this.templateData, this.lastTemplateId, this.categoryList, - {super.key}); + const TemplateView(this.templateData, this.lastTemplateId, {super.key}); @override State createState() => _TemplateViewState(); } class _TemplateViewState extends State { + final _controller = Get.find(); + final Map _icons = { 'auto_stories': Icons.auto_stories, 'auto_graph': Icons.auto_graph, @@ -38,7 +39,7 @@ class _TemplateViewState extends State { 'auto_fix_high': Icons.auto_fix_high, }; - final List _categoryTemplateList = []; + List _categoryTemplateList = []; Map _templateData = {}; @override @@ -47,8 +48,9 @@ class _TemplateViewState extends State { _getTemplate(); } - void _getTemplate() async { - for (var element in widget.categoryList) { + void _getTemplate() { + _categoryTemplateList = []; + for (var element in _controller.categoryTemplateList) { List templateList = []; bool isAdd = false; if (element['id'] == widget.templateData['categoryid']) { @@ -71,7 +73,28 @@ class _TemplateViewState extends State { 'template': templateList, }); } - _templateData = widget.templateData; + setState(() { + _templateData = widget.templateData; + }); + } + + void _returnCallback() { + if (_controller.editTemplate.isNotEmpty) { + _getTemplate(); + if (_controller.editTemplate['id'] == _templateData['id']) { + _templateData['name'] = _controller.editTemplate['name']; + _templateData['desc'] = _controller.editTemplate['desc']; + _templateData['prompt'] = _controller.editTemplate['prompt']; + } + } + if (_controller.deleteTemplateId != 0) { + _getTemplate(); + if (_controller.deleteTemplateId == _templateData['id']) { + _templateData = {}; + } + _categoryTemplateList.last['template'].removeWhere( + (element) => element['id'] == _controller.deleteTemplateId); + } } @override @@ -127,13 +150,15 @@ class _TemplateViewState extends State { padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w), child: GestureDetector( onTap: () { - Get.back(result: _templateData); + if (_templateData.isNotEmpty) { + Get.back(result: _templateData); + } }, child: Container( height: 48.w, alignment: Alignment.center, decoration: BoxDecoration( - color: Colors.black, + color: _templateData.isNotEmpty ? Colors.black : Colors.grey, borderRadius: BorderRadius.circular(12.r), ), child: Text( @@ -155,13 +180,14 @@ class _TemplateViewState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ GestureDetector( - onTap: () { - Get.to(() => TemplateAllView( + onTap: () async { + await Get.to(() => TemplateAllView( data['name'], data['id'], _templateData, widget.lastTemplateId, )); + _returnCallback(); }, child: Padding( padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.w), @@ -189,7 +215,7 @@ class _TemplateViewState extends State { child: isLast && data['template'].length == 0 ? Padding( padding: EdgeInsets.symmetric(horizontal: 8.w), - child: _createTemplate(), + child: _createTemplate(data['id']), ) : ListView.builder( itemCount: data['template'].length, @@ -200,11 +226,11 @@ class _TemplateViewState extends State { return isLast && index == 0 ? Row( children: [ - _createTemplate(), - _template(template), + _createTemplate(data['id']), + _template(template, true), ], ) - : _template(template); + : _template(template, false); }, ), ), @@ -212,7 +238,7 @@ class _TemplateViewState extends State { ); } - Widget _template(Map template) { + Widget _template(Map template, bool isEdit) { return Container( width: 200.w, margin: EdgeInsets.symmetric(horizontal: 4.w), @@ -309,8 +335,10 @@ class _TemplateViewState extends State { top: 0, right: 0, child: GestureDetector( - onTap: () { - Get.to(() => TemplateDetailsView(template)); + onTap: () async { + await Get.to( + () => TemplateDetailsView(template, isEdit: isEdit)); + _returnCallback(); }, child: Padding( padding: EdgeInsets.all(10.w), @@ -371,10 +399,13 @@ class _TemplateViewState extends State { ); } - Widget _createTemplate() { + Widget _createTemplate(int categoryid) { return GestureDetector( - onTap: () { - Get.to(() => const TemplateEditView()); + onTap: () async { + await Get.to(() => TemplateEditView(categoryid: categoryid)); + if (_controller.editTemplate.isNotEmpty) { + _getTemplate(); + } }, child: Container( width: 200.w, diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 0ed92e073..396d3ecc2 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -480,13 +480,14 @@ object AgentService : CoroutineScope { "phone_call" -> { // 通话模式:禁用空闲检测,保持持续激活 Log.d(TAG, "通话模式:禁用空闲检测") + startIdleCheck(mode,maxIdleSeconds) return } else -> { // 未知模式:使用默认的空闲检测 Log.w(TAG, "未知识别模式: $mode,使用默认空闲检测") - startIdleCheck(maxIdleSeconds) + startIdleCheck(mode,maxIdleSeconds) } } } @@ -494,7 +495,7 @@ object AgentService : CoroutineScope { /** * 启动空闲检测 */ - private fun startIdleCheck(idleSeconds: Int = maxIdleSeconds) { + private fun startIdleCheck(mode: String,idleSeconds: Int = maxIdleSeconds) { stopIdleCheck() // 先停止现有的检查 if (!isRecognitionActive) return @@ -503,8 +504,8 @@ object AgentService : CoroutineScope { idleCheckJob = launch { try { delay(idleSeconds * 1000L) - - if (isActive && isRecognitionActive && !isTtsSpeaking && !isAiStreaming) { + Log.d(TAG, "空闲检测触发自动停止,isActive: $isActive, isRecognitionActive: $isRecognitionActive, isTtsSpeaking: $isTtsSpeaking, isAiStreaming: $isAiStreaming, mode: $mode") + if (isActive && isRecognitionActive && !isTtsSpeaking && !isAiStreaming && mode != "phone_call") { Log.d(TAG, "空闲检测触发自动停止,模式: $currentRecognitionMode") BleService.closeCodec() stopRecognition() @@ -517,6 +518,10 @@ object AgentService : CoroutineScope { ) ) } + if (isActive && isRecognitionActive && !isTtsSpeaking && !isAiStreaming && mode == "phone_call") { + Log.d(TAG, "空闲检测触发自动停止,模式: $currentRecognitionMode") + startRecognition(false, mode) + } } catch (e: CancellationException) { // 协程被取消,正常行为 } catch (e: Exception) { @@ -531,6 +536,7 @@ object AgentService : CoroutineScope { private fun stopIdleCheck() { idleCheckJob?.cancel() idleCheckJob = null + Log.d(TAG, "空闲检测已停止") } /** @@ -643,11 +649,9 @@ object AgentService : CoroutineScope { override fun onRecognizing(sessionid:String, recognizing: String, detectedLanguage: String) { if (recognizing.isNotEmpty()&&isKeepResult==true) { // 检测到语音,更新状态 - - _hasSpeechDetected.set(true) - - Log.d(TAG, "ai识别中重置定时器") - restartIdleCheck() + _hasSpeechDetected.set(true) + Log.d(TAG, "ai识别中重置定时器") + restartIdleCheck() sendEvent( diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt index 76219fcb3..09fc9ebe6 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt @@ -1006,7 +1006,7 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { val pcmData = extractPcmFromWav(audioData) if (pcmData.isNotEmpty()) { FileLogger.d(tag, "提取PCM数据,大小=${pcmData.size}字节") - BleService.writeExternalRightAudioData(pcmData) + BleService.writeExternalAudioData(pcmData) } else { FileLogger.e(tag, "提取PCM数据失败,音频数据可能不是WAV格式") diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift index a34ee7790..e55663cc4 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift @@ -18,7 +18,7 @@ import ble_service // 日志标签 private let tag = "AzureSpeechPlugin" // 日志对象 - private let log = OSLog(subsystem: "com.azure.speech", category: "AzureSpeechPlugin") + internal let log = OSLog(subsystem: "com.azure.speech", category: "AzureSpeechPlugin") // MARK: - 防抖相关(iOS 原生层) // 函数级注释:用于限制 startContinuousRecognition/stopContinuousRecognition 在极短时间内的重复触发 @@ -269,7 +269,7 @@ private func sendAstEvent(_ event: [String: Any]) { * @param wavData WAV格式的音频数据 * @return 纯PCM音频数据,如果提取失败则返回空Data */ - private func extractPcmFromWav(_ wavData: Data) -> Data { + internal func extractPcmFromWav(_ wavData: Data) -> Data { // WAV文件头最小长度为44字节 guard wavData.count >= 44 else { os_log("WAV数据长度不足,无法解析文件头", log: log, type: .error) @@ -1011,15 +1011,15 @@ private class AstEventCallback: IntegratedSpeechTranslationService.ServiceEventC public func onSynthesisAudioGenerated(text: String, audioData: Data) { guard let plugin = plugin else { return } - plugin.os_log("语音合成音频生成,文本=%@,音频数据大小=%d", log: plugin.log, type: .info, text, audioData.count) + os_log("语音合成音频生成,文本=%@,音频数据大小=%d", log: plugin.log, type: .info, text, audioData.count) // Azure TTS返回的是RIFF WAV格式,需要去除文件头提取PCM数据 let pcmData = plugin.extractPcmFromWav(audioData) if !pcmData.isEmpty { - plugin.os_log("提取PCM数据,大小=%d字节", log: plugin.log, type: .info, pcmData.count) + os_log("提取PCM数据,大小=%d字节", log: plugin.log, type: .info, pcmData.count) BleService.shared.writeExternalAudioData(data: pcmData) } else { - plugin.os_log("提取PCM数据失败,音频数据可能不是WAV格式", log: plugin.log, type: .error) + os_log("提取PCM数据失败,音频数据可能不是WAV格式", log: plugin.log, type: .error) } // plugin?.sendAsrEvent([ diff --git a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift index 1e5f2260d..40bcfbc9e 100644 --- a/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift +++ b/local_plugins/music_service/ios/music_service/Sources/music_service/MusicService.swift @@ -37,14 +37,9 @@ public class MusicService: NSObject { private override init() { super.init() - - // 确保应用能接收远程控制事件 - UIApplication.shared.beginReceivingRemoteControlEvents() - setupRemoteCommandCenter() setupAudioSession() - - print("🎵 [MusicService] 🚀 MusicService初始化完成,远程控制已激活") + setupNotificationObservers() } public func initialize(serviceUrl: String, userToken: String) { @@ -53,34 +48,34 @@ public class MusicService: NSObject { print("MusicService initialized with url: \(serviceUrl)") } - - // MARK: - 音频会话设置 - private func setupAudioSession() { - print("🎵 [MusicService] 🚀 开始设置音频会话和通知监听器") - - // 先注册通知监听器,不依赖音频会话设置成功 - setupNotificationObservers() - - // 然后尝试设置音频会话 - setupAudioSessionConfiguration() - - // 确保媒体控制中心可以接收到我们的应用 - setupRemoteCommandCenter() - - print("🎵 [MusicService] 📋 所有通知观察者注册完成") - print("🎵 [MusicService] 🔍 当前MusicService实例: \(Unmanaged.passUnretained(self).toOpaque())") + + // MARK: - 音频会话设置 + private func setupAudioSession() { + do { + // 设置音频会话类别,添加 mixWithOthers 选项以确保通知正常显示 + try AVAudioSession.sharedInstance().setCategory( + .playback, + mode: .default, + options: [.allowAirPlay, .allowBluetooth, .mixWithOthers] + ) + try AVAudioSession.sharedInstance().setActive(true) + print("🎵 [MusicService] ✅ 音频会话设置成功") + } catch { + print("🎵 [MusicService] ❌ 设置音频会话失败: \(error)") + // 尝试备用配置 + do { + try AVAudioSession.sharedInstance().setCategory(.playback) + try AVAudioSession.sharedInstance().setActive(true) + print("🎵 [MusicService] ✅ 使用备用音频会话配置") + } catch { + print("🎵 [MusicService] ❌ 备用音频会话配置也失败: \(error)") + } + } } + private func setupNotificationObservers() { - // 监听系统音频中断(来电/通话/Siri等) - NotificationCenter.default.addObserver( - self, - selector: #selector(handleAudioInterruption(_:)), - name: AVAudioSession.interruptionNotification, - object: nil - ) print("🎵 [MusicService] ✅ 已注册系统音频中断通知") - // 注册AudioSessionHub的自定义中断通知 NotificationCenter.default.addObserver( self, @@ -108,31 +103,6 @@ public class MusicService: NSObject { print("🎵 [MusicService] ✅ 已注册AudioSessionHub_ResumeMusicPlayback通知") } - private func setupAudioSessionConfiguration() { - do { - let audioSession = AVAudioSession.sharedInstance() - - // 使用更简单的配置,避免参数冲突 - try audioSession.setCategory(.playback, mode: .default, options: [.allowAirPlay, .allowBluetooth, .allowBluetoothA2DP]) - print("🎵 [MusicService] ✅ 音频会话类别设置成功") - - // 延迟激活音频会话,避免与其他服务冲突 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - do { - try audioSession.setActive(true) - print("🎵 [MusicService] ✅ 音频会话激活成功") - } catch { - print("🎵 [MusicService] ⚠️ 音频会话激活失败: \(error)") - // 激活失败不影响通知系统工作 - } - } - - } catch { - print("🎵 [MusicService] ❌ 设置音频会话失败: \(error)") - print("🎵 [MusicService] ℹ️ 音频会话设置失败不影响通知系统工作") - } - } - // MARK: - 基础播放控制 func getCurrentUrlString() -> String? { guard let urlAsset = player?.currentItem?.asset as? AVURLAsset else { @@ -144,7 +114,7 @@ public class MusicService: NSObject { func getCurrentId() -> Int64? { return currentIdInternal } - + // MARK: - 歌曲搜索 public func searchSongs(keyword: String, searchType: Int = 0, completion: @escaping (Result<[[String: Any?]], Error>) -> Void) { @@ -256,7 +226,7 @@ public class MusicService: NSObject { }else{ completion(.failure(error)) } - + return } @@ -326,7 +296,7 @@ public class MusicService: NSObject { task.resume() } - + private func parseUrlFromResponse(jsonResponse: [String: Any]) -> [String: Any?] { var urlData: [String: Any?] = [:] @@ -340,7 +310,7 @@ public class MusicService: NSObject { return urlData } - + public func playPlaylist(songs: [[String: Any]]) { playlist = songs currentIndex = 0 @@ -391,7 +361,7 @@ public class MusicService: NSObject { // 使用新 URL 开始播放 self?.startPlayback(song: updatedSong, url: newUrl) } - + } else { print("获取到的 URL 为空") } @@ -405,7 +375,7 @@ public class MusicService: NSObject { startPlayback(song: song, url: url!) } } - + // 新增方法:更新 playlist 中指定 ID 的歌曲数据 private func updatePlaylistSong(songId: Int64, updatedSong: [String: Any]) { for i in 0.. [String: Any] { guard let player = player, let currentItem = player.currentItem else { @@ -588,21 +527,21 @@ public class MusicService: NSObject { "progress": 0.0 ] } - + let currentTime = CMTimeGetSeconds(player.currentTime()) let duration = CMTimeGetSeconds(currentItem.duration) - + // 更严格的有效性检查 let currentPosition = (currentTime.isNaN || currentTime.isInfinite || currentTime < 0) ? 0 : Int(currentTime * 1000) let totalDuration = (duration.isNaN || duration.isInfinite || duration <= 0) ? 0 : Int(duration * 1000) let progress = (totalDuration > 0 && currentTime >= 0) ? currentTime / duration : 0.0 - + let result: [String: Any] = [ // Add explicit type annotation here - "currentPosition": currentPosition, - "duration": totalDuration, - "progress": progress.isNaN || progress.isInfinite ? 0.0 : progress - ] - + "currentPosition": currentPosition, + "duration": totalDuration, + "progress": progress.isNaN || progress.isInfinite ? 0.0 : progress + ] + // 添加调试日志 if totalDuration == 0 { @@ -611,7 +550,7 @@ public class MusicService: NSObject { return result } - + func seekTo(positionMs: Int) { guard let player = player else { return } @@ -624,221 +563,216 @@ public class MusicService: NSObject { if completed { print("跳转到位置: \(positionMs)ms") - // 如果之前在播放,跳转后继续播放 + // 如果之前是播放状态,确保继续播放 if wasPlaying { - self?.resume() + self?.player?.play() + self?.isPlaying = true + self?.updateMediaSessionPlaybackState(isPlaying: true) + self?.startProgressTimer() } - } else { - print("跳转失败") + + // 立即更新进度信息到系统媒体控制中心 + self?.updateProgressInfoImmediately() + + // 通知Flutter层 + self?.flutterChannel?.invokeMethod("onMediaSessionSeekTo", arguments: ["position": positionMs]) } } } - // MARK: - 远程控制中心设置 + // MARK: - 媒体会话控制 private func setupRemoteCommandCenter() { let commandCenter = MPRemoteCommandCenter.shared() - // 清除之前的目标,避免重复注册 + // 清除之前的目标,避免重复添加 commandCenter.playCommand.removeTarget(nil) commandCenter.pauseCommand.removeTarget(nil) commandCenter.nextTrackCommand.removeTarget(nil) commandCenter.previousTrackCommand.removeTarget(nil) - commandCenter.changePlaybackPositionCommand.removeTarget(nil) + commandCenter.stopCommand.removeTarget(nil) + + // 启用命令 + commandCenter.playCommand.isEnabled = true + commandCenter.pauseCommand.isEnabled = true + commandCenter.nextTrackCommand.isEnabled = true + commandCenter.previousTrackCommand.isEnabled = true + commandCenter.stopCommand.isEnabled = true - // 播放命令 commandCenter.playCommand.addTarget { [weak self] _ in - print("🎵 [RemoteControl] 播放命令") + print("🎵 [RemoteCommand] 播放命令触发") self?.resume() return .success } - // 暂停命令 commandCenter.pauseCommand.addTarget { [weak self] _ in - print("🎵 [RemoteControl] 暂停命令") + print("🎵 [RemoteCommand] 暂停命令触发") self?.pause() return .success } - // 下一首命令 commandCenter.nextTrackCommand.addTarget { [weak self] _ in - print("🎵 [RemoteControl] 下一首命令") - self?.playNext() + print("🎵 [RemoteCommand] 下一首命令触发") + self?.next() return .success } - // 上一首命令 commandCenter.previousTrackCommand.addTarget { [weak self] _ in - print("🎵 [RemoteControl] 上一首命令") - self?.playPrevious() + print("🎵 [RemoteCommand] 上一首命令触发") + self?.previous() return .success } - // 进度调整命令(iOS 9.1+) - if #available(iOS 9.1, *) { - commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in - if let positionEvent = event as? MPChangePlaybackPositionCommandEvent { - let positionMs = Int(positionEvent.positionTime * 1000) - print("🎵 [RemoteControl] 跳转到 \(positionMs)ms") - self?.seekTo(positionMs: positionMs) - return .success - } - return .commandFailed - } - commandCenter.changePlaybackPositionCommand.isEnabled = true - } - - // 启用远程控制命令 - commandCenter.playCommand.isEnabled = true - commandCenter.pauseCommand.isEnabled = true - commandCenter.nextTrackCommand.isEnabled = true - commandCenter.previousTrackCommand.isEnabled = true - - // 确保音频会话支持远程控制 - do { - let audioSession = AVAudioSession.sharedInstance() - try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers]) - print("🎵 [RemoteControl] 音频会话配置成功") - } catch { - print("🎵 [RemoteControl] ⚠️ 音频会话配置失败: \(error)") + commandCenter.stopCommand.addTarget { [weak self] _ in + print("🎵 [RemoteCommand] 停止命令触发") + self?.stop() + return .success } - print("🎵 [RemoteControl] ✅ 远程控制中心设置完成") + print("🎵 [MusicService] ✅ 远程控制中心设置完成") } - // MARK: - 媒体信息更新 private func updateNowPlayingInfo(song: [String: Any]) { - var nowPlayingInfo = [String: Any]() - - // 设置歌曲信息 - nowPlayingInfo[MPMediaItemPropertyTitle] = song["name"] as? String ?? "未知歌曲" - nowPlayingInfo[MPMediaItemPropertyArtist] = song["singer"] as? String ?? "未知歌手" + let title = song["title"] as? String ?? song["name"] as? String ?? "未知标题" + let artist = song["artist"] as? String ?? song["sgener"] as? String ?? "未知歌手" + let coverUrl = song["coverUrl"] as? String ?? song["image"] as? String ?? "" - // 设置专辑信息(如果有的话) - if let album = song["album"] as? String, !album.isEmpty { - nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = album - } + var nowPlayingInfo: [String: Any] = [ + MPMediaItemPropertyTitle: title, + MPMediaItemPropertyArtist: artist + ] - // 设置播放时间信息 - if let player = player, let currentItem = player.currentItem { - let currentTime = CMTimeGetSeconds(player.currentTime()) - let duration = CMTimeGetSeconds(currentItem.duration) - - if !currentTime.isNaN && !currentTime.isInfinite { - nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime - } - - if !duration.isNaN && !duration.isInfinite && duration > 0 { - nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration + // 异步加载封面图片 + if !coverUrl.isEmpty, let url = URL(string: coverUrl) { + DispatchQueue.global(qos: .background).async { + if let data = try? Data(contentsOf: url), + let image = UIImage(data: data) { + DispatchQueue.main.async { + nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image } + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + } + } else { + DispatchQueue.main.async { + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + } + } } - } - - // 设置播放速率 - nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0 - - // 设置媒体类型为音频 - nowPlayingInfo[MPMediaItemPropertyMediaType] = MPMediaType.music.rawValue - - // 强制更新媒体信息 - MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo - - // 确保在主线程上再次设置,解决某些iOS版本的兼容性问题 - DispatchQueue.main.async { + } else { MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo } - - print("🎵 [MediaInfo] ✅ 已更新媒体信息: \(song["name"] as? String ?? "未知")") - print("🎵 [MediaInfo] 📱 播放状态: \(isPlaying ? "播放中" : "暂停")") } private func updatePlaybackState() { - guard let flutterChannel = flutterChannel else { return } - - let progressInfo = getProgress() - - flutterChannel.invokeMethod("onPlaybackStateChanged", arguments: [ - "isPlaying": isPlaying, - "currentPosition": progressInfo["currentPosition"] ?? 0, - "duration": progressInfo["duration"] ?? 0, - "progress": progressInfo["progress"] ?? 0.0 - ]) + // iOS manages playback state automatically, but you can update NowPlayingInfo if needed } + // MARK: - 新增媒体会话功能 + + /// 更新媒体会话进度信息 public func updateMediaSessionProgress(currentPosition: Int, duration: Int, progress: Float) { - guard let flutterChannel = flutterChannel else { return } + var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] - flutterChannel.invokeMethod("onProgressChanged", arguments: [ - "currentPosition": currentPosition, - "duration": duration, - "progress": progress - ]) + // 更新播放时长和当前位置 + nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = Double(duration) / 1000.0 + nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = Double(currentPosition) / 1000.0 + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0 + + // 确保在主线程更新 + DispatchQueue.main.async { + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + print("🎵 [MusicService] ✅ 媒体会话进度已更新: \(currentPosition/1000)s/\(duration/1000)s") + } } + /// 启用媒体会话回调 public func enableMediaSessionCallbacks(channel: FlutterMethodChannel) { - self.flutterChannel = channel + flutterChannel = channel + setupEnhancedRemoteCommandCenter() + startProgressTimer() print("媒体会话回调已启用") } + /// 更新媒体会话元数据 public func updateMediaSessionMetadata(song: [String: Any]) { - guard let flutterChannel = flutterChannel else { return } + let title = song["title"] as? String ?? song["name"] as? String ?? "未知标题" + let artist = song["artist"] as? String ?? song["sgener"] as? String ?? "未知歌手" + let coverUrl = song["coverUrl"] as? String ?? song["image"] as? String ?? "" - // 构建媒体元数据 - var metadata: [String: Any] = [:] - - // 基本信息 - if let id = song["id"] { - metadata["id"] = id - } - if let name = song["name"] as? String { - metadata["title"] = name - } - if let singer = song["singer"] as? String { - metadata["artist"] = singer - } - if let image = song["image"] as? String { - metadata["artUri"] = image + // 更新当前歌曲ID + if let id = song["id"] as? NSNumber { + currentIdInternal = id.int64Value } - // 播放状态 - metadata["isPlaying"] = isPlaying + print("updateMediaSessionMetadata currentIdInternal: \(currentIdInternal ?? 0)") - // 进度信息 - let progressInfo = getProgress() - metadata["currentPosition"] = progressInfo["currentPosition"] ?? 0 - metadata["duration"] = progressInfo["duration"] ?? 0 - metadata["progress"] = progressInfo["progress"] ?? 0.0 - - // 播放列表信息 - metadata["playlistSize"] = playlist.count - metadata["currentIndex"] = currentIndex - - // 能力标志 - metadata["canSkipToNext"] = currentIndex < playlist.count - 1 - metadata["canSkipToPrevious"] = currentIndex > 0 - metadata["canSeek"] = true + var nowPlayingInfo: [String: Any] = [ + MPMediaItemPropertyTitle: title, + MPMediaItemPropertyArtist: artist + ] - flutterChannel.invokeMethod("onMetadataChanged", arguments: metadata) + // 异步加载封面图片 + if !coverUrl.isEmpty, let url = URL(string: coverUrl) { + print("开始加载封面图片: \(coverUrl)") + + DispatchQueue.global(qos: .background).async { + // 创建带超时的URLRequest + var request = URLRequest(url: url) + request.timeoutInterval = 10.0 + + URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + print("图片加载失败: \(error.localizedDescription)") + DispatchQueue.main.async { + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + } + return + } + + guard let data = data, let image = UIImage(data: data) else { + print("图片数据无效或格式不支持") + DispatchQueue.main.async { + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + } + return + } + + print("图片加载成功") + DispatchQueue.main.async { + nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image } + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + } + }.resume() + } + } else { + print("封面图片URL为空") + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + } - print("已更新媒体元数据: \(metadata)") + print("媒体会话元数据已更新: \(title) - \(artist)") } + /// 更新媒体会话播放状态 public func updateMediaSessionPlaybackState(isPlaying: Bool) { - guard let flutterChannel = flutterChannel else { return } + self.isPlaying = isPlaying - let progressInfo = getProgress() - - let playbackState: [String: Any] = [ - "isPlaying": isPlaying, - "currentPosition": progressInfo["currentPosition"] ?? 0, - "duration": progressInfo["duration"] ?? 0, - "progress": progressInfo["progress"] ?? 0.0, - "playbackSpeed": isPlaying ? 1.0 : 0.0 - ] + // 确保在主线程更新 + DispatchQueue.main.async { + var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] + nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0 + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo + + print("🎵 [MusicService] ✅ 媒体会话播放状态已更新: \(isPlaying ? "播放中" : "已暂停")") + } - flutterChannel.invokeMethod("onPlaybackStateChanged", arguments: playbackState) + if isPlaying { + startProgressTimer() + } else { + stopProgressTimer() + } - print("已更新播放状态: isPlaying=\(isPlaying)") + print("媒体会话播放状态已更新: \(isPlaying)") } + /// 设置增强的远程控制中心 private func setupEnhancedRemoteCommandCenter() { let commandCenter = MPRemoteCommandCenter.shared() @@ -851,54 +785,54 @@ public class MusicService: NSObject { // 播放命令 commandCenter.playCommand.addTarget { [weak self] _ in - print("远程控制: 播放") self?.resume() + self?.flutterChannel?.invokeMethod("onMediaSessionPlay", arguments: nil) return .success } // 暂停命令 commandCenter.pauseCommand.addTarget { [weak self] _ in - print("远程控制: 暂停") self?.pause() + self?.flutterChannel?.invokeMethod("onMediaSessionPause", arguments: nil) return .success } - // 下一首命令 + // 下一曲命令 commandCenter.nextTrackCommand.addTarget { [weak self] _ in - print("远程控制: 下一首") - self?.playNext() + self?.next() + self?.flutterChannel?.invokeMethod("onMediaSessionNext", arguments: nil) return .success } - // 上一首命令 + // 上一曲命令 commandCenter.previousTrackCommand.addTarget { [weak self] _ in - print("远程控制: 上一首") - self?.playPrevious() + self?.previous() + self?.flutterChannel?.invokeMethod("onMediaSessionPrevious", arguments: nil) return .success } - // 进度调整命令 + // 跳转命令 - 关键升级点 + commandCenter.changePlaybackPositionCommand.isEnabled = true commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in - if let positionEvent = event as? MPChangePlaybackPositionCommandEvent { - let positionMs = Int(positionEvent.positionTime * 1000) - print("远程控制: 跳转到 \(positionMs)ms") + if let event = event as? MPChangePlaybackPositionCommandEvent { + let positionMs = Int(event.positionTime * 1000) self?.seekTo(positionMs: positionMs) return .success } return .commandFailed } - // 启用命令 - commandCenter.playCommand.isEnabled = true - commandCenter.pauseCommand.isEnabled = true - commandCenter.nextTrackCommand.isEnabled = playlist.count > 1 - commandCenter.previousTrackCommand.isEnabled = playlist.count > 1 - commandCenter.changePlaybackPositionCommand.isEnabled = true + // 停止命令 + commandCenter.stopCommand.addTarget { [weak self] _ in + self?.stop() + self?.flutterChannel?.invokeMethod("onMediaSessionStop", arguments: nil) + return .success + } - print("增强远程控制中心设置完成") + print("增强的远程控制中心已设置,支持进度条滑动") } - // MARK: - 进度定时器 + /// 启动进度定时器 private func startProgressTimer() { stopProgressTimer() progressTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in @@ -906,217 +840,169 @@ public class MusicService: NSObject { } } + /// 停止进度定时器 private func stopProgressTimer() { progressTimer?.invalidate() progressTimer = nil } + /// 更新进度信息 private func updateProgressInfo() { - guard isPlaying, let player = player, let currentItem = player.currentItem else { - return - } + guard let player = player, let currentItem = player.currentItem, isPlaying else { return } let currentTime = CMTimeGetSeconds(player.currentTime()) let duration = CMTimeGetSeconds(currentItem.duration) - if !currentTime.isNaN && !currentTime.isInfinite && - !duration.isNaN && !duration.isInfinite && duration > 0 { + if !currentTime.isNaN && !currentTime.isInfinite && !duration.isNaN && !duration.isInfinite { + // 更新系统媒体信息 - 关键升级点 + updateSystemMediaInfo(currentTime: currentTime, duration: duration) + // 通知Flutter层 let currentPosition = Int(currentTime * 1000) let totalDuration = Int(duration * 1000) - let progress = Float(currentTime / duration) - - // 更新媒体会话进度 - updateMediaSessionProgress(currentPosition: currentPosition, - duration: totalDuration, - progress: progress) + let progress = totalDuration > 0 ? Float(currentTime / duration) : 0.0 - // 更新系统媒体信息 - updateSystemMediaInfo(currentTime: currentTime, duration: duration) + flutterChannel?.invokeMethod("onMediaSessionProgress", arguments: [ + "currentIdInternal": currentIdInternal ?? 0, + "currentPosition": currentPosition, + "duration": totalDuration, + "progress": progress + ]) } } + /// 立即更新进度信息(用于seekTo后) private func updateProgressInfoImmediately() { - guard let player = player, let currentItem = player.currentItem else { - return - } + guard let player = player, let currentItem = player.currentItem else { return } let currentTime = CMTimeGetSeconds(player.currentTime()) let duration = CMTimeGetSeconds(currentItem.duration) - updateSystemMediaInfo(currentTime: currentTime, duration: duration) + if !currentTime.isNaN && !currentTime.isInfinite && !duration.isNaN && !duration.isInfinite { + updateSystemMediaInfo(currentTime: currentTime, duration: duration) + } } + /// 更新系统媒体信息 private func updateSystemMediaInfo(currentTime: Double, duration: Double) { var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [:] - - if !currentTime.isNaN && !currentTime.isInfinite { - nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime - } - - if !duration.isNaN && !duration.isInfinite && duration > 0 { - nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration - } - + nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime + nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = isPlaying ? 1.0 : 0.0 - MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo } - // MARK: - 播放器观察者 + // MARK: - 播放完成处理 private func setupPlayerObservers() { guard let player = player else { return } - // 监听播放器状态 - player.addObserver(self, forKeyPath: "timeControlStatus", options: [.new], context: nil) - - // 监听播放结束通知 NotificationCenter.default.addObserver( - self, - selector: #selector(playerDidFinishPlaying), - name: .AVPlayerItemDidPlayToEndTime, - object: player.currentItem - ) + forName: .AVPlayerItemDidPlayToEndTime, + object: player.currentItem, + queue: .main + ) { [weak self] _ in + self?.stopProgressTimer() + self?.playNextOrStop() + } } + // MARK: - 播放项观察者设置 private func setupPlayerItemObservers(playerItem: AVPlayerItem, song: [String: Any]) { + // 移除之前的观察者 + NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil) + NotificationCenter.default.removeObserver(self, name: .AVPlayerItemFailedToPlayToEndTime, object: nil) + // 监听播放项状态 - playerItem.addObserver(self, forKeyPath: "status", options: [.new], context: nil) + playerItem.addObserver(self, forKeyPath: "status", options: [.new, .initial], context: nil) + playerItem.addObserver(self, forKeyPath: "duration", options: [.new], context: nil) + playerItem.addObserver(self, forKeyPath: "loadedTimeRanges", options: [.new], context: nil) - // 监听缓冲状态 - playerItem.addObserver(self, forKeyPath: "playbackBufferEmpty", options: [.new], context: nil) - playerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: [.new], context: nil) - } - - @objc private func playerDidFinishPlaying() { - print("歌曲播放完成") - isPlaying = false - updatePlaybackState() - updateMediaSessionPlaybackState(isPlaying: false) - stopProgressTimer() + // 播放完成通知 + NotificationCenter.default.addObserver( + forName: .AVPlayerItemDidPlayToEndTime, + object: playerItem, + queue: .main + ) { [weak self] _ in + print("歌曲播放完成") + self?.stopProgressTimer() + self?.playNextOrStop() + } - // 自动播放下一首或停止 - playNextOrStop() + // 播放失败通知 + NotificationCenter.default.addObserver( + forName: .AVPlayerItemFailedToPlayToEndTime, + object: playerItem, + queue: .main + ) { [weak self] notification in + print("播放失败: \(notification)") + self?.isPlaying = false + self?.updateMediaSessionPlaybackState(isPlaying: false) + } + + // 存储歌曲信息,用于状态变化时更新 + objc_setAssociatedObject(playerItem, "songInfo", song, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } // MARK: - KVO 观察者 override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { - guard let keyPath = keyPath else { return } + guard let playerItem = object as? AVPlayerItem else { return } switch keyPath { case "status": - if let playerItem = object as? AVPlayerItem { - switch playerItem.status { - case .readyToPlay: - print("🎵 [Player] 播放器准备就绪,开始播放") - player?.play() - isPlaying = true - updatePlaybackState() - updateMediaSessionPlaybackState(isPlaying: true) - startProgressTimer() - - // 强制更新媒体信息,确保通知栏显示 - if let currentSong = playlist.isEmpty ? nil : playlist[currentIndex] { - updateNowPlayingInfo(song: currentSong) - updateMediaSessionMetadata(song: currentSong) - print("🎵 [Player] ✅ 播放开始时已更新媒体信息") - } - - // 再次确保远程控制激活 - DispatchQueue.main.async { - UIApplication.shared.beginReceivingRemoteControlEvents() - print("🎵 [Player] ✅ 远程控制事件接收已重新激活") - } - - case .failed: - print("播放器加载失败: \(playerItem.error?.localizedDescription ?? "未知错误")") - isPlaying = false - updatePlaybackState() - updateMediaSessionPlaybackState(isPlaying: false) - - case .unknown: - print("播放器状态未知") - - @unknown default: - break + switch playerItem.status { + case .readyToPlay: + print("播放项准备就绪,开始播放") + + // 获取关联的歌曲信息 + if let song = objc_getAssociatedObject(playerItem, "songInfo") as? [String: Any] { + updateNowPlayingInfo(song: song) } + + // 开始播放 + player?.play() + isPlaying = true + updateMediaSessionPlaybackState(isPlaying: true) + startProgressTimer() + + case .failed: + print("播放项加载失败: \(playerItem.error?.localizedDescription ?? "未知错误")") + isPlaying = false + updateMediaSessionPlaybackState(isPlaying: false) + + case .unknown: + print("播放项状态未知") + + @unknown default: + break } - case "timeControlStatus": - if let player = object as? AVPlayer { - switch player.timeControlStatus { - case .playing: - print("播放器正在播放") - if !isPlaying { - isPlaying = true - updatePlaybackState() - updateMediaSessionPlaybackState(isPlaying: true) - startProgressTimer() - } - - case .paused: - print("播放器已暂停") - if isPlaying { - isPlaying = false - updatePlaybackState() - updateMediaSessionPlaybackState(isPlaying: false) - stopProgressTimer() - } - - case .waitingToPlayAtSpecifiedRate: - print("播放器等待缓冲") - - @unknown default: - break - } + case "duration": + let duration = CMTimeGetSeconds(playerItem.duration) + if !duration.isNaN && !duration.isInfinite && duration > 0 { + print("获取到音频时长: \(duration) 秒") + // 更新系统媒体信息 + updateProgressInfoImmediately() } - case "playbackBufferEmpty": - print("播放缓冲区为空") - - case "playbackLikelyToKeepUp": - if let playerItem = object as? AVPlayerItem, playerItem.isPlaybackLikelyToKeepUp { - print("播放缓冲充足,可以继续播放") + case "loadedTimeRanges": + if let timeRange = playerItem.loadedTimeRanges.first?.timeRangeValue { + let loadedDuration = CMTimeGetSeconds(CMTimeAdd(timeRange.start, timeRange.duration)) + let totalDuration = CMTimeGetSeconds(playerItem.duration) + if !totalDuration.isNaN && !totalDuration.isInfinite && totalDuration > 0 { + let loadedPercentage = loadedDuration / totalDuration * 100 + print("缓冲进度: \(String(format: "%.1f", loadedPercentage))%") + } } default: - super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) - } - } - - // MARK: - 音频中断处理 - @objc private func handleAudioInterruption(_ notification: Notification) { - print("🎵 [MusicService] 收到系统音频中断通知") - - guard let info = notification.userInfo, - let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt, - let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { - print("🎵 [MusicService] 无法解析中断通知信息") - return - } - - switch type { - case .began: - print("🎵 [MusicService] 系统音频中断开始 - 当前播放状态: \(isPlaying)") - // 记录中断发生时是否在播放,并暂停 - interruptedDuringPlayback = isPlaying - if isPlaying { - pause() - print("🎵 [MusicService] 已暂停音乐播放") - } - - case .ended: - print("🎵 [MusicService] 系统音频中断结束 - 等待AudioSessionHub处理") - // 系统中断结束时不立即处理,等待AudioSessionHub的通知 - - @unknown default: - print("🎵 [MusicService] 未知的中断类型: \(typeValue)") break } } // 处理AudioSessionHub发送的中断开始通知 @objc private func handleAudioSessionHubInterruption(_ notification: Notification) { + interruptedDuringPlayback = isPlaying + pause() print("🎵 [MusicService] 收到AudioSessionHub中断开始通知") // AudioSessionHub已经处理了中断开始,我们这里可以做额外的处理 } @@ -1146,9 +1032,9 @@ public class MusicService: NSObject { do { // 设置音频会话为播放模式 - let audioSession = AVAudioSession.sharedInstance() - try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers]) - try audioSession.setActive(true, options: []) +// let audioSession = AVAudioSession.sharedInstance() +// try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers]) +// try audioSession.setActive(true, options: []) print("🎵 [MusicService] 音频会话已设置为播放模式") // 恢复播放 @@ -1187,9 +1073,9 @@ public class MusicService: NSObject { do { // 设置音频会话为播放模式 - let audioSession = AVAudioSession.sharedInstance() - try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers]) - try audioSession.setActive(true, options: []) +// let audioSession = AVAudioSession.sharedInstance() +// try audioSession.setCategory(.playback, mode: .default, options: [.mixWithOthers]) +// try audioSession.setActive(true, options: []) print("🎵 [MusicService] 音频会话已设置为播放模式") // 恢复播放 @@ -1211,12 +1097,6 @@ public class MusicService: NSObject { } deinit { - NotificationCenter.default.removeObserver( - self, - name: AVAudioSession.interruptionNotification, - object: nil - ) - NotificationCenter.default.removeObserver( self, name: NSNotification.Name("AudioSessionInterruptionBegan"),