From fce86a81be033f2de21fbebf21c59102fadbd85a Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Tue, 13 May 2025 16:10:17 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E9=9F=B3=E4=B9=90?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=BC=82=E5=B8=B8=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../music_service/android/src/main/AndroidManifest.xml | 3 ++- .../kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/local_plugins/music_service/android/src/main/AndroidManifest.xml b/local_plugins/music_service/android/src/main/AndroidManifest.xml index 3469dab1e..3154f0c1d 100644 --- a/local_plugins/music_service/android/src/main/AndroidManifest.xml +++ b/local_plugins/music_service/android/src/main/AndroidManifest.xml @@ -7,7 +7,8 @@ + android:exported="true" + android:foregroundServiceType="mediaPlayback"> diff --git a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt index 15d02734e..c94ebb3d4 100644 --- a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt +++ b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt @@ -718,7 +718,7 @@ class MCPClient(private val context: Context? = null) : AutoCloseable { */ suspend fun callTool(name: String, arguments: Map): JSONObject? { val requestId = "${System.currentTimeMillis()}-${(0..1000).random()}" - Log.d(TAG, "[$requestId] 调用工具: $name") + Log.d(TAG, "[$requestId] 调用工具: $name 参数:$arguments") val toolType = getToolType(name) From 1d165139d4835d02e96770dfd682a7afa2e72088 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Tue, 13 May 2025 17:29:45 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat(=E4=BC=9A=E8=AE=AE=E6=A8=A1=E5=9D=97):?= =?UTF-8?q?=20tanls=E9=87=8D=E6=9E=84=E4=BC=9A=E8=AE=AE=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=EF=BC=8C=E6=96=B0=E5=A2=9E=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=92=8C=E4=BC=9A=E8=AE=AE=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E8=A7=86=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次提交对会议模块进行了重构,主要包括以下内容: 1. 新增文件上传功能,支持通过腾讯云COS上传文件。 2. 新增会议详情视图,包括会议概览、思维导图、总结和发言内容等标签页。 3. 优化数据库结构,新增会议发言者表,并调整了会议和会议详情表的结构。 4. 移除不再使用的会议编辑和会议记录功能,简化了路由和页面结构。 5. 新增会议发言者命名功能,支持对发言者进行命名和编辑。 --- lib/core/utils/upload_oss.dart | 88 +++ lib/data/services/db/database_helper.dart | 26 +- lib/data/services/db/sqflite_api.dart | 109 ++-- .../bindings/meeting_edit_binding.dart | 11 - .../bindings/meeting_record_binding.dart | 10 - .../controllers/meeting_controller.dart | 79 ++- .../meeting_details_controller.dart | 361 ++++++++++- .../controllers/meeting_edit_controller.dart | 143 ----- .../meeting_record_controller.dart | 324 ---------- .../meeting/model/meeting_details_model.dart | 30 +- lib/modules/meeting/model/meeting_model.dart | 21 +- .../meeting/views/meeting_details_view.dart | 566 ++++++++++-------- .../meeting/views/meeting_edit_view.dart | 301 ---------- .../meeting/views/meeting_record_view.dart | 363 ----------- lib/modules/meeting/views/meeting_view.dart | 322 +++++----- lib/modules/meeting/views/mind_map_tab.dart | 12 + lib/modules/meeting/views/overview_tab.dart | 12 + .../meeting/views/speaker_bottom_sheet.dart | 202 +++++++ lib/modules/meeting/views/speech_tab.dart | 256 ++++++++ lib/modules/meeting/views/summary_tab.dart | 226 +++++++ lib/routes/app_pages.dart | 18 - lib/routes/app_routes.dart | 4 +- pubspec.yaml | 1 + 23 files changed, 1768 insertions(+), 1717 deletions(-) create mode 100644 lib/core/utils/upload_oss.dart delete mode 100644 lib/modules/meeting/bindings/meeting_edit_binding.dart delete mode 100644 lib/modules/meeting/bindings/meeting_record_binding.dart delete mode 100644 lib/modules/meeting/controllers/meeting_edit_controller.dart delete mode 100644 lib/modules/meeting/controllers/meeting_record_controller.dart delete mode 100644 lib/modules/meeting/views/meeting_edit_view.dart delete mode 100644 lib/modules/meeting/views/meeting_record_view.dart create mode 100644 lib/modules/meeting/views/mind_map_tab.dart create mode 100644 lib/modules/meeting/views/overview_tab.dart create mode 100644 lib/modules/meeting/views/speaker_bottom_sheet.dart create mode 100644 lib/modules/meeting/views/speech_tab.dart create mode 100644 lib/modules/meeting/views/summary_tab.dart diff --git a/lib/core/utils/upload_oss.dart b/lib/core/utils/upload_oss.dart new file mode 100644 index 000000000..1adeefe8e --- /dev/null +++ b/lib/core/utils/upload_oss.dart @@ -0,0 +1,88 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:common_utils/common_utils.dart'; +import 'package:dio/dio.dart'; +import 'package:tencentcloud_cos_sdk_plugin/cos.dart'; +import 'package:tencentcloud_cos_sdk_plugin/cos_transfer_manger.dart'; +import 'package:tencentcloud_cos_sdk_plugin/pigeon.dart'; + +class UploadOss { + static final UploadOss _shared = UploadOss._internal(); + factory UploadOss() => _shared; + + static const bucket = 'deepsound-1253517901'; + static const region = 'ap-guangzhou'; + + static CosTransferManger? _cosTransferManger; + + UploadOss._internal(); + + static Future getTransferManger() async { + if (_cosTransferManger == null) { + await Cos().initWithPlainSecret('AKID9F8qNiqlTzIMFwQ9rQxNcgYCdsukNmzA', + 'OYVGo8HEzWpvpfHgtfHfqpcDbnlY7Tdc'); + if (Cos().hasTransferManger(region)) { + _cosTransferManger = Cos().getTransferManger(region); + } else { + CosXmlServiceConfig serviceConfig = CosXmlServiceConfig( + region: region, + isHttps: true, + ); + _cosTransferManger = await Cos().registerTransferManger( + region, serviceConfig..region = region, TransferConfig()); + } + } + return _cosTransferManger!; + } + + static Future upload({ + String? filepath, + String rootDir = 'file', + String? fileType, + Function? callback, + ProgressCallback? onSendProgress, + }) async { + String pathName = + '$rootDir/${getDate()}/${getRandom(12)}.${fileType ?? getFileType(filepath!)}'; + CosTransferManger cosTransferManger = await getTransferManger(); + final completer = Completer(); + await cosTransferManger.upload( + bucket, + pathName, + filePath: filepath, + resultListener: ResultListener( + (Map? header, CosXmlResult? result) { + final url = result?.accessUrl ?? + 'https://$bucket.cos.$region.myqcloud.com/$pathName'; + completer.complete(url); + }, + (clientException, serviceException) { + completer.completeError(clientException ?? + serviceException ?? + Exception('Unknown error')); + }, + ), + ); + return completer.future; + } + + static String getDate() { + DateTime now = DateTime.now(); + return DateUtil.formatDate(now, format: 'yyyy/MM/dd'); + } + + static String getRandom(int num) { + String alphabet = 'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM'; + String left = ''; + for (var i = 0; i < num; i++) { + left = left + alphabet[Random().nextInt(alphabet.length)]; + } + return left; + } + + static String getFileType(String path) { + List array = path.split('.'); + return array[array.length - 1]; + } +} diff --git a/lib/data/services/db/database_helper.dart b/lib/data/services/db/database_helper.dart index 2c62e6709..5257c524c 100644 --- a/lib/data/services/db/database_helper.dart +++ b/lib/data/services/db/database_helper.dart @@ -30,11 +30,10 @@ class DatabaseHelper { CREATE TABLE IF NOT EXISTS meeting ( id INTEGER PRIMARY KEY, title TEXT, - content TEXT, + type TEXT, seconds INTEGER, - address TEXT, - longitude TEXT, - latitude TEXT, + filepath TEXT, + audiourl TEXT, creationtime INTEGER ) '''); @@ -43,9 +42,24 @@ class DatabaseHelper { CREATE TABLE IF NOT EXISTS meetingdetails ( id INTEGER PRIMARY KEY, meetingid INTEGER, - audio TEXT, + address TEXT, + personnel TEXT, + taskid TEXT, + tasktype INTEGER, + overview TEXT, + summary TEXT, + mindmap TEXT + ) + '''); + // 商务会议助手转写内容 + batch.execute(''' + CREATE TABLE IF NOT EXISTS meetingspeaker ( + id INTEGER PRIMARY KEY, + meetingid INTEGER, content TEXT, - aisummary TEXT + starttime INTEGER, + endtime INTEGER, + speaker TEXT ) '''); await batch.commit(); diff --git a/lib/data/services/db/sqflite_api.dart b/lib/data/services/db/sqflite_api.dart index 5af8dbf8f..cdb2e8d57 100644 --- a/lib/data/services/db/sqflite_api.dart +++ b/lib/data/services/db/sqflite_api.dart @@ -31,26 +31,32 @@ class SqfliteApi { }; } + // 获取商务会议助手转写内容 + static Future getMeetingSpeaker(id) async { + final db = await DatabaseHelper().database; + return await db.query( + 'meetingspeaker', + where: 'meetingid = ?', + whereArgs: [id], + ); + } + // 插入商务会议助手 static Future insertMeeting(Map meeting) async { final db = await DatabaseHelper().database; try { int id = 0; await db.transaction((txn) async { - id = await txn.insert('meeting', { - 'title': meeting['title'], - 'content': meeting['firstContent'], - 'seconds': meeting['seconds'], - 'address': meeting['address'], - 'longitude': meeting['longitude'], - 'latitude': meeting['latitude'], - 'creationtime': meeting['creationTime'], - }); + id = await txn.insert('meeting', meeting); await txn.insert('meetingdetails', { 'meetingid': id, - 'audio': meeting['audio'], - 'content': meeting['content'], - 'aisummary': meeting['aisummary'], + 'address': '', + 'personnel': '', + 'taskid': '', + 'tasktype': 0, + 'overview': '', + 'summary': '', + 'mindmap': '', }); }); return id; @@ -66,34 +72,65 @@ class SqfliteApi { } } - // 修改商务会议助手 + // 修改商务会议助手详情内容 static Future editMeeting(id, Map meeting) async { final db = await DatabaseHelper().database; try { - await db.transaction((txn) async { - await txn.update( - 'meeting', - { - 'title': meeting['title'], - 'content': meeting['firstContent'], - 'seconds': meeting['seconds'], - 'address': meeting['address'], - 'longitude': meeting['longitude'], - 'latitude': meeting['latitude'], - }, + await db.update( + 'meetingdetails', + meeting, + where: 'meetingid = ?', + whereArgs: [id], + ); + return true; + } catch (e) { + Get.snackbar( + 'error'.tr, // 错误 + 'unknownError'.tr, + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); + return; + } + } + + // 插入商务会议助手转写内容 + static Future insertMeetingSpeaker(List> data) async { + final db = await DatabaseHelper().database; + try { + final batch = db.batch(); + for (var item in data) { + batch.insert('meetingspeaker', item); + } + await batch.commit(); + return true; + } catch (e) { + Get.snackbar( + 'error'.tr, // 错误 + 'unknownError'.tr, + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); + return; + } + } + + // 修改商务会议助手转写内容 + static Future editMeetingSpeaker(List data) async { + final db = await DatabaseHelper().database; + try { + final batch = db.batch(); + for (var item in data) { + batch.update( + 'meetingspeaker', + {'content': item['content']}, where: 'id = ?', - whereArgs: [id], - ); - await txn.update( - 'meetingdetails', - { - 'content': meeting['content'], - 'aisummary': meeting['aisummary'], - }, - where: 'meetingid = ?', - whereArgs: [id], + whereArgs: [item['id']], ); - }); + } + await batch.commit(); return true; } catch (e) { Get.snackbar( @@ -121,6 +158,8 @@ class SqfliteApi { await txn.delete('meeting', where: 'id = ?', whereArgs: [id]); await txn .delete('meetingdetails', where: 'meetingid = ?', whereArgs: [id]); + await txn + .delete('meetingspeaker', where: 'meetingid = ?', whereArgs: [id]); }); } } diff --git a/lib/modules/meeting/bindings/meeting_edit_binding.dart b/lib/modules/meeting/bindings/meeting_edit_binding.dart deleted file mode 100644 index 31d1443ee..000000000 --- a/lib/modules/meeting/bindings/meeting_edit_binding.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:get/get.dart'; - -import '../controllers/meeting_edit_controller.dart'; - -class MeetingEditBinding extends Bindings { - @override - void dependencies() { - // 注册控制器 - Get.lazyPut(() => MeetingEditController()); - } -} diff --git a/lib/modules/meeting/bindings/meeting_record_binding.dart b/lib/modules/meeting/bindings/meeting_record_binding.dart deleted file mode 100644 index 3fd92a57c..000000000 --- a/lib/modules/meeting/bindings/meeting_record_binding.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:get/get.dart'; -import '../controllers/meeting_record_controller.dart'; - -class MeetingRecordBinding extends Bindings { - @override - void dependencies() { - // 注册控制器 - Get.lazyPut(() => MeetingRecordController()); - } -} diff --git a/lib/modules/meeting/controllers/meeting_controller.dart b/lib/modules/meeting/controllers/meeting_controller.dart index e798e6f36..766e55327 100644 --- a/lib/modules/meeting/controllers/meeting_controller.dart +++ b/lib/modules/meeting/controllers/meeting_controller.dart @@ -1,10 +1,19 @@ +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:get/get.dart'; +import 'package:just_audio/just_audio.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; +import '../../../core/utils/upload_oss.dart'; import '../../../data/services/db/sqflite_api.dart'; class MeetingController extends GetxController { final ScrollController listController = ScrollController(); + final AudioPlayer _audioPlayer = AudioPlayer(); // 是否还有更多数据 bool isLoadingMore = true; @@ -18,6 +27,7 @@ class MeetingController extends GetxController { void onInit() { super.onInit(); listController.addListener(_onScroll); + Permission.storage.request(); _getDataList(); } @@ -25,6 +35,7 @@ class MeetingController extends GetxController { void onClose() { listController.dispose(); titleController.dispose(); + _audioPlayer.dispose(); super.onClose(); } @@ -48,23 +59,75 @@ class MeetingController extends GetxController { } } + void addAudio() async { + EasyLoading.show(); + FilePickerResult? result = await FilePicker.platform.pickFiles( + withData: true, + ); + if (result != null) { + PlatformFile file = result.files.first; + String filePath = await _saveFile(file); + String fileName = file.name; + await _audioPlayer.setAudioSource(AudioSource.file(filePath)); + var audiourl = + await UploadOss.upload(filepath: filePath, rootDir: 'audio'); + int seconds = _audioPlayer.duration?.inSeconds ?? 0; + int creationTime = DateTime.now().millisecondsSinceEpoch; + Map data = { + 'title': fileName, + 'type': 'NOTE', + 'seconds': seconds, + 'filepath': filePath, + 'audiourl': audiourl, + 'creationtime': creationTime, + }; + final meetingId = await SqfliteApi.insertMeeting(data); + if (meetingId != null) { + dataList.insert(0, { + 'id': meetingId, + ...data, + }); + } + } + EasyLoading.dismiss(); + } + + Future _saveFile(PlatformFile file) async { + final directory = await getApplicationDocumentsDirectory(); + final String filePath = '${directory.path}/${file.name}'; + final File localFile = File(filePath); + await localFile.writeAsBytes(file.bytes!); + return filePath; + } + // 修改标题 void editMeetingTitle(int id, String title) async { - if (title.isEmpty) return; - SqfliteApi.editMeetingTitle(id, title); - int index = dataList.indexWhere((item) => item['id'] == id); - if (index != -1) { - final dataMap = Map.from(dataList[index]); - dataMap['title'] = title; - dataList[index] = dataMap; + if (title.isNotEmpty) { + SqfliteApi.editMeetingTitle(id, title); + int index = dataList.indexWhere((item) => item['id'] == id); + if (index != -1) { + final dataMap = Map.from(dataList[index]); + dataMap['title'] = title; + dataList[index] = dataMap; + } } Get.back(); } // 删除数据 - void deleteMeeting(int id) { + void deleteMeeting(int id, String filepath) async { dataList.removeWhere((item) => item['id'] == id); // 删除数据库中的记录 SqfliteApi.deleteMeeting(id); + _deleteFile(filepath); + } + + // 删除文件 + Future _deleteFile(String filepath) async { + File file = File(filepath); + bool fileExists = await file.exists(); + if (fileExists) { + await file.delete(); + } } } diff --git a/lib/modules/meeting/controllers/meeting_details_controller.dart b/lib/modules/meeting/controllers/meeting_details_controller.dart index c1319036d..e602c00eb 100644 --- a/lib/modules/meeting/controllers/meeting_details_controller.dart +++ b/lib/modules/meeting/controllers/meeting_details_controller.dart @@ -1,67 +1,374 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:get/get.dart'; -import 'package:just_audio/just_audio.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:http/http.dart' as http; +import 'package:uuid/uuid.dart'; +import '../../../core/utils/logger.dart'; +import '../../../core/utils/upload_oss.dart'; +import '../../../data/models/appconfig_model.dart'; +import '../../../data/services/ai_service.dart'; import '../../../data/services/db/sqflite_api.dart'; +import '../../../data/services/open_ai_service_adapter.dart'; import '../model/meeting_details_model.dart'; import '../model/meeting_model.dart'; class MeetingDetailsController extends GetxController with GetSingleTickerProviderStateMixin { - late AnimationController lottieController; - final AudioPlayer audioPlayer = AudioPlayer(); + late ScrollController scrollController; + late TabController tabController; + final RxInt currentIndex = 0.obs; + + final PlayerController playerController = PlayerController(); + RxBool isTop = false.obs; + RxInt durationMax = 0.obs; int id = 0; RxBool isLoading = true.obs; Rx meetingData = MeetingModel().obs; - Rx dataDetails = MeetingDetailsModel().obs; + Rx meetingDetails = MeetingDetailsModel().obs; + RxList textList = [].obs; + String allText = ''; + RxBool isSpeakerText = false.obs; + List editSpeakerTextList = []; + RxInt editSpeakerTextId = 0.obs; + String editSpeakerTextChange = ''; + + final AiService _aiService = Get.find(); + // Agent 信息 + late final DBAgent _agent; + String _userId = ''; + StreamSubscription? _aiResponseSubscription; - int seconds = 0; + bool _isDisposed = false; - RxBool isPlay = false.obs; + final String _appid = '2316081933'; + final String _token = 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En'; + final String _cluster = 'volc_auc_meeting_flash'; + final String _serviceUrl = 'https://openspeech.bytedance.com/api/v1/auc'; + Map _headers = {}; @override void onInit() { super.onInit(); - lottieController = AnimationController(vsync: this); + scrollController = ScrollController(initialScrollOffset: 0.0); + scrollController.addListener(_onScroll); + tabController = TabController(length: 4, vsync: this); + tabController.addListener(() { + currentIndex.value = tabController.index; + }); final Map args = Get.arguments ?? {}; id = args['id']; + _headers = { + 'Authorization': 'Bearer; $_token', + 'Content-Type': 'application/json', + }; + _getAgent(); _getDataDetails(); } @override void onClose() { - lottieController.dispose(); - audioPlayer.dispose(); + scrollController.dispose(); + tabController.dispose(); + playerController.dispose(); + _aiResponseSubscription?.cancel(); + _isDisposed = true; super.onClose(); } + void _onScroll() async { + double pixels = scrollController.position.pixels; + double maxScrollExtent = scrollController.position.maxScrollExtent; + if (!isTop.value && pixels > 5) { + await scrollController.animateTo( + maxScrollExtent, + duration: const Duration(milliseconds: 100), + curve: Curves.easeOut, + ); + if (pixels > maxScrollExtent - 2) { + isTop.value = true; + } + } + if (isTop.value && pixels < maxScrollExtent - 5) { + await scrollController.animateTo( + 0, + duration: const Duration(milliseconds: 100), + curve: Curves.easeOut, + ); + if (pixels < 2) { + isTop.value = false; + } + } + } + + void _getAgent() async { + final GetStorage storage = GetStorage(); + final List agents = storage.read("AGENTS"); + Map userInfo = storage.read("user_info"); + _userId = userInfo['user']['uid']; + _agent = agents.firstWhere((agent) => agent.id == 'meet_assistant'); + } + void _getDataDetails() async { final data = await SqfliteApi.getMeetingDetails(id); meetingData.value = MeetingModel.fromJson(data); - dataDetails.value = MeetingDetailsModel.fromJson(data['details']); - audioPlayer.setAudioSource(AudioSource.file(dataDetails.value.audio)); - audioPlayer.playerStateStream.listen((state) async { - if (state.processingState == ProcessingState.completed) { - await audioPlayer.seek(Duration.zero); - audioPlayer.pause(); - lottieController.stop(); - isPlay.value = false; - } - }); + meetingDetails.value = MeetingDetailsModel.fromJson(data['details']); + await playerController.preparePlayer( + path: meetingData.value.filepath, + noOfSamples: 1200, + ); + durationMax.value = await playerController.getDuration(DurationType.max); + playerController.setFinishMode(finishMode: FinishMode.pause); + if (meetingDetails.value.tasktype == 1) { + List speakerList = await SqfliteApi.getMeetingSpeaker(id); + textList.value = List.from(speakerList); + } isLoading.value = false; } - void start() async { - if (isPlay.value) { - lottieController.stop(); - audioPlayer.pause(); - isPlay.value = false; + void editSpeakerText() async { + if (editSpeakerTextId.value != 0 && editSpeakerTextChange.isNotEmpty) { + Map? foundElement = editSpeakerTextList.firstWhere( + (element) => element['id'] == editSpeakerTextId.value, + orElse: () => null, + ); + if (foundElement != null) { + foundElement['content'] = editSpeakerTextChange; + } else { + editSpeakerTextList.add({ + 'id': editSpeakerTextId.value, + 'content': editSpeakerTextChange, + }); + } + } + if (editSpeakerTextList.isNotEmpty) { + EasyLoading.show(); + var result = await SqfliteApi.editMeetingSpeaker(editSpeakerTextList); + if (result != null) { + for (var element in editSpeakerTextList) { + int index = + textList.indexWhere((item) => item['id'] == element['id']); + if (index != -1) { + final dataMap = Map.from(textList[index]); + dataMap['content'] = element['content']; + textList[index] = dataMap; + } + } + isSpeakerText.value = false; + editSpeakerTextId.value = 0; + editSpeakerTextList = []; + } + EasyLoading.dismiss(); } else { - lottieController.repeat(); - audioPlayer.play(); - isPlay.value = true; + isSpeakerText.value = false; + editSpeakerTextId.value = 0; + editSpeakerTextList = []; + } + } + + void editSpeakerName() {} + + void speakerCreate() async { + EasyLoading.show(); + await _audioSpeaker(); + EasyLoading.dismiss(); + } + + void summaryCreate() async { + EasyLoading.show(); + if (meetingDetails.value.tasktype != 1) { + await _audioSpeaker(); + } + if (allText.isEmpty) { + String text = ''; + for (var element in textList) { + if (element['speaker'].isNotEmpty) { + text += '${element['speaker']}:'; + } + text += '${element['content']}\n'; + } + allText = text; + } + _aiServiceListen(); + // const uuid = Uuid(); + // _aiService.send( + // sid: uuid.v4(), + // messages: [ + // {'role': 'user', 'content': allText}, + // ], + // systemPrompt: _agent.systemPrompt, + // ); + } + + void _aiServiceListen() { + String summaryText = ''; + // _aiResponseSubscription = _aiService.start().listen((AIStreamEvent event) { + // summaryText += event.content ?? ''; + // if (event.type == StreamEventType.complete) { + // SqfliteApi.editMeeting(id, {'summary': summaryText}); + // meetingDetails.value.summary = summaryText; + // meetingDetails.refresh(); + // EasyLoading.dismiss(); + // } + // }); + } + + Future _audioSpeaker() async { + String audiourl = meetingData.value.audiourl; + if (audiourl.isEmpty) { + audiourl = await UploadOss.upload( + filepath: meetingData.value.filepath, + rootDir: 'audio', + ); + meetingData.value.audiourl = audiourl; + } + String taskId = meetingDetails.value.taskid; + if (taskId.isEmpty) { + taskId = await _submitTask(audiourl); + meetingDetails.value.taskid = taskId; + await SqfliteApi.editMeeting(id, {'taskid': taskId}); + } + await _fileRecognize(taskId); + } + + Future _submitTask(String audiourl) async { + String format = _getFileType(audiourl); + + final requestBody = jsonEncode({ + "app": { + "appid": _appid, + "token": _token, + "cluster": _cluster, + }, + "user": { + "uid": _userId, + }, + "audio": { + "format": format, + "url": audiourl, + }, + "additions": { + "with_speaker_info": "True", + } + }); + + try { + final response = await http.post( + Uri.parse('$_serviceUrl/submit'), + headers: _headers, + body: requestBody, + ); + + 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'); + return taskId; + } catch (e) { + Logger.error('Submit error: $e'); + rethrow; + } + } + + String _getFileType(String path) { + List array = path.split('.'); + return array[array.length - 1]; + } + + Future> _queryTask(String taskId) async { + final queryBody = jsonEncode({ + "appid": _appid, + "token": _token, + "id": taskId, + "cluster": _cluster, + }); + + try { + final response = await http.post( + Uri.parse('$_serviceUrl/query'), + headers: _headers, + body: queryBody, + ); + + if (response.statusCode != 200) { + throw Exception('Query failed with status: ${response.statusCode}'); + } + + final respDic = jsonDecode(response.body); + return respDic; + } catch (e) { + Logger.error('Query error: $e'); + rethrow; + } + } + + Future _fileRecognize(String taskId) async { + try { + final startTime = DateTime.now().millisecondsSinceEpoch; + + while (!_isDisposed) { + await Future.delayed(const Duration(seconds: 2)); + + if (_isDisposed) return; + + final respDic = await _queryTask(taskId); + final code = respDic['resp']['code'] as int; + + if (code == 1000) { + await _addTextList(respDic['resp']['utterances']); + return; + } else if (code < 2000) { + Logger.debug("FileRecognize failed"); + return; + } + + final currentTime = DateTime.now().millisecondsSinceEpoch; + if (currentTime - startTime > 300 * 1000) { + Logger.debug('wait time exceeds 300s'); + return; + } + } + } catch (e) { + Logger.error('Error occurred: $e'); + } + } + + Future _addTextList(List utterances) async { + String text = ''; + List> list = []; + for (var element in utterances) { + String speaker = ''; + if (element['additions'].isNotEmpty) { + speaker = 'Speaker ${element['additions']['speaker']}'; + text += '$speaker:'; + } + list.add({ + 'meetingid': id, + 'content': element['text'], + 'starttime': element['start_time'], + 'endtime': element['end_time'], + 'speaker': speaker, + }); + text += '${element['text']}\n'; + } + var result = await SqfliteApi.insertMeetingSpeaker(list); + if (result != null) { + await SqfliteApi.editMeeting(id, {'tasktype': 1}); + List speakerList = await SqfliteApi.getMeetingSpeaker(id); + textList.value = List.from(speakerList); + meetingDetails.value.tasktype = 1; + meetingDetails.refresh(); } + allText = text; } } diff --git a/lib/modules/meeting/controllers/meeting_edit_controller.dart b/lib/modules/meeting/controllers/meeting_edit_controller.dart deleted file mode 100644 index d214735bc..000000000 --- a/lib/modules/meeting/controllers/meeting_edit_controller.dart +++ /dev/null @@ -1,143 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_easyloading/flutter_easyloading.dart'; -import 'package:get/get.dart'; -import 'package:just_audio/just_audio.dart'; - -import '../../../data/services/ai_service.dart'; -import '../../../data/services/db/sqflite_api.dart'; -import '../../../data/services/volcano_ai_service.dart'; -import 'meeting_controller.dart'; -import 'meeting_details_controller.dart'; - -class MeetingEditController extends GetxController - with GetSingleTickerProviderStateMixin { - late AiService _aiService; - - final MeetingController meetingController = Get.find(); - final MeetingDetailsController meetingDetailsController = - Get.find(); - - late AnimationController lottieController; - - late TextEditingController titleController; - late TextEditingController contentController; - - final AudioPlayer audioPlayer = AudioPlayer(); - - int creationTime = 0; - - RxString address = ''.obs; - String _longitude = ''; - String _latitude = ''; - - int id = 0; - - RxBool isPlay = false.obs; - - @override - void onInit() { - super.onInit(); - lottieController = AnimationController(vsync: this); - final Map args = Get.arguments ?? {}; - id = args['id']; - _getDataDetails(); - } - - @override - void onClose() { - lottieController.dispose(); - titleController.dispose(); - contentController.dispose(); - audioPlayer.dispose(); - super.onClose(); - } - - void _getDataDetails() async { - titleController = TextEditingController( - text: meetingDetailsController.meetingData.value.title, - ); - contentController = TextEditingController( - text: meetingDetailsController.dataDetails.value.content, - ); - creationTime = meetingDetailsController.meetingData.value.creationtime; - address.value = meetingDetailsController.meetingData.value.address; - _longitude = meetingDetailsController.meetingData.value.longitude; - _latitude = meetingDetailsController.meetingData.value.latitude; - audioPlayer.setAudioSource( - AudioSource.file(meetingDetailsController.dataDetails.value.audio)); - audioPlayer.playerStateStream.listen((state) async { - if (state.processingState == ProcessingState.completed) { - await audioPlayer.seek(Duration.zero); - audioPlayer.pause(); - lottieController.stop(); - isPlay.value = false; - } - }); - } - - void start() async { - if (isPlay.value) { - lottieController.stop(); - audioPlayer.pause(); - isPlay.value = false; - } else { - lottieController.repeat(); - audioPlayer.play(); - isPlay.value = true; - } - } - - void confirm() async { - EasyLoading.show(); - _aiService = Get.find(); - String aiSummary = await _aiService.sendMessage( - systemPrompt: '', - messages: [ - {'role': 'user', 'content': contentController.text}, - {'role': 'user', 'content': '帮我总结一下这次会议'}, - ], - ); - String content = contentController.text; - String firstContent = - content.length >= 30 ? content.substring(0, 30) : content; - String title = titleController.text.isNotEmpty - ? titleController.text - : meetingDetailsController.meetingData.value.title; - Map data = { - 'title': title, - 'firstContent': firstContent.replaceAll("\n", ""), - 'seconds': meetingDetailsController.seconds, - 'content': contentController.text, - 'aisummary': aiSummary, - 'address': address.value, - 'longitude': _longitude, - 'latitude': _latitude, - }; - final result = await SqfliteApi.editMeeting(id, data); - if (result != null) { - int index = meetingController.dataList.indexWhere( - (item) => item['id'] == id, - ); - if (index != -1) { - final dataMap = - Map.from(meetingController.dataList[index]); - dataMap['title'] = title; - dataMap['content'] = firstContent.replaceAll("\n", ""); - dataMap['seconds'] = meetingDetailsController.seconds; - dataMap['address'] = address.value; - meetingController.dataList[index] = dataMap; - } - meetingDetailsController.meetingData.value.title = title; - meetingDetailsController.meetingData.value.address = address.value; - meetingDetailsController.meetingData.value.longitude = _longitude; - meetingDetailsController.meetingData.value.latitude = _latitude; - meetingDetailsController.dataDetails.value.content = - contentController.text; - meetingDetailsController.dataDetails.value.aisummary = aiSummary; - meetingDetailsController.meetingData.refresh(); - meetingDetailsController.dataDetails.refresh(); - Get.back(); - } - EasyLoading.dismiss(); - } -} diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart deleted file mode 100644 index a83e1c312..000000000 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ /dev/null @@ -1,324 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:amap_flutter/amap_flutter.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_easyloading/flutter_easyloading.dart'; -import 'package:get/get.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:permission_handler/permission_handler.dart'; - -import '../../../core/utils/logger.dart'; -import '../../../data/services/ai_service.dart'; -import '../../../data/services/asr_service.dart'; -import '../../../data/services/db/sqflite_api.dart'; -import '../../../data/services/speech_impl/xunfei_asr_service.dart'; -import '../../../data/services/volcano_ai_service.dart'; -import 'meeting_controller.dart'; - -class MeetingRecordController extends GetxController - with GetSingleTickerProviderStateMixin { - late AiService _aiService; - - final MeetingController meetingController = Get.find(); - - late AnimationController lottieController; - late AsrService _asrService; - - final _amapFlutter = AmapFlutter(); - StreamSubscription? _locationStream; - - Timer? timer; - - RxBool isRecording = false.obs; //是否录音中 - RxInt seconds = 0.obs; - - String title = ''; - int creationTime = 0; - - RxString address = '正在定位'.obs; - String _longitude = ''; - String _latitude = ''; - - RxString intermediateContent = ''.obs; - RxString finalContent = ''.obs; - - StreamSubscription? _recognitionSubscription; - - BytesBuilder _originaBytes = BytesBuilder(); //原始音频 - - @override - void onInit() { - super.onInit(); - Permission.storage.request(); - lottieController = AnimationController(vsync: this); - Get.put(XunfeiAsrService()); - _asrService = Get.find(); - final Map args = Get.arguments ?? {}; - title = args['title']; - creationTime = DateTime.now().millisecondsSinceEpoch; - _amapInit(); - } - - @override - void onClose() { - lottieController.dispose(); - timer?.cancel(); - _asrService.dispose(); - _recognitionSubscription?.cancel(); - _originaBytes = BytesBuilder(); - _amapFlutter.dispose(); - _locationStream?.cancel(); - super.onClose(); - } - - void _amapInit() async { - await _amapFlutter.initLocation(); - _locationStream = _amapFlutter.locationStream().listen((data) { - address.value = data['address']; - _longitude = data['longitude'].toString(); - _latitude = data['latitude'].toString(); - _amapFlutter.stopLocation(); - }); - _amapFlutter.startLocation(); - } - - // 点击开始、暂停按钮 - void recorderHandle() { - if (isRecording.value) { - _pause(); - } else { - _start(); - } - } - - // 开始 - void _start() async { - await _startRecorder(); - lottieController.repeat(); - isRecording.value = true; - timer = Timer.periodic(const Duration(seconds: 1), (_) => seconds++); - } - - // 暂停 - Future _pause() async { - await _stopRecorder(); - lottieController.stop(); - isRecording.value = false; - timer?.cancel(); - } - - //开始录音 - Future _startRecorder() async { - final recognitionStream = - await _asrService.startContinuousRecognition(false); - _recognitionSubscription = recognitionStream.listen( - _handleRecognitionEvent, - ); - } - - //结束录音 - Future _stopRecorder() async { - await _asrService.stopContinuousRecognition(); - } - - // 处理语音识别事件 - void _handleRecognitionEvent(RecognitionEvent event) { - switch (event.type) { - case RecognitionEventType.intermediateResult: - intermediateContent.value = event.text; - break; - case RecognitionEventType.finalResult: - finalContent.value += _spliceText(event); - intermediateContent.value = ''; - _originaBytes.add(event.audio!); - break; - default: - // 不做任何处理 - break; - } - } - - String _spliceText(RecognitionEvent event) { - String eventText = event.text; - String text = ''; - if (finalContent.value.isNotEmpty) { - if (event.role != '') { - final punctuations = { - ',', - '。', - '!', - '?', - ';', - ':', - ',', - '.', - ':', - ';', - '!', - '?' - }; - final firstChar = eventText[0]; - if (punctuations.contains(firstChar)) { - text += '$firstChar\n'; - eventText = eventText.substring(1); - } else { - text += '\n'; - } - text += '说话人${event.role}:'; - } - } else { - text += '说话人${event.role}:'; - } - text += eventText; - return text; - } - - // 完成 - void complete() async { - EasyLoading.show(); - await _pause(); - _aiSummary(); - } - - void _aiSummary() async { - _aiService = Get.find(); - String aiSummary = await _aiService.sendMessage( - systemPrompt: '', - messages: [ - {'role': 'user', 'content': finalContent.value}, - {'role': 'user', 'content': '帮我总结一下这次会议'}, - ], - ); - _save(aiSummary); - } - - void _save(String aiSummary) async { - String firstContent = finalContent.value.length >= 30 - ? finalContent.value.substring(0, 30) - : finalContent.value; - Uint8List audio = _originaBytes.toBytes(); - Uint8List wavHeader = generateWavHeader( - sampleRate: 16000, - numberOfChannels: 1, - bitsPerSample: 16, - dataLength: audio.length, - ); - Uint8List wavData = Uint8List.fromList([...wavHeader, ...audio]); - String audiopPath = - await _saveAudioToFile(wavData, 'meeting_$creationTime.wav'); - if (audiopPath.isNotEmpty) { - Map data = { - 'title': title, - 'firstContent': firstContent.replaceAll("\n", ""), - 'seconds': seconds.value, - 'creationTime': creationTime, - 'audio': audiopPath, - 'content': finalContent.value, - 'aisummary': aiSummary, - 'address': address.value, - 'longitude': _longitude, - 'latitude': _latitude, - }; - final result = await SqfliteApi.insertMeeting(data); - if (result != null) { - meetingController.dataList.insert(0, { - 'id': result, - 'title': title, - 'content': firstContent.replaceAll("\n", ""), - 'seconds': seconds.value, - 'address': address.value, - 'longitude': _longitude, - 'latitude': _latitude, - 'creationtime': creationTime, - }); - Get.back(); - } - } - EasyLoading.dismiss(); - } - - Future _saveAudioToFile(Uint8List audioData, String fileName) async { - try { - final directory = await getApplicationDocumentsDirectory(); - final file = File('${directory.path}/$fileName'); - - // 写入文件 - await file.writeAsBytes(audioData); - return file.path; - } catch (e) { - Logger.error('保存失败: $e'); - return ''; - } - } - - Uint8List generateWavHeader({ - required int sampleRate, - required int numberOfChannels, - required int bitsPerSample, - required int dataLength, - }) { - final byteRate = sampleRate * numberOfChannels * (bitsPerSample ~/ 8); - final blockAlign = numberOfChannels * (bitsPerSample ~/ 8); - final riffSize = 36 + dataLength; - final header = Uint8List(44); - var pos = 0; - // RIFF 块 - header.setRange(pos, pos + 4, [0x52, 0x49, 0x46, 0x46]); // 'RIFF' - pos += 4; - // 写入 RIFF 大小(小端) - header.setRange(pos, pos + 4, [ - riffSize & 0xff, - (riffSize >> 8) & 0xff, - (riffSize >> 16) & 0xff, - (riffSize >> 24) & 0xff, - ]); - pos += 4; - // WAVE 标识 - header.setRange(pos, pos + 4, [0x57, 0x41, 0x56, 0x45]); // 'WAVE' - pos += 4; - // fmt 子块 - header.setRange(pos, pos + 4, [0x66, 0x6d, 0x74, 0x20]); // 'fmt ' - pos += 4; - header.setRange(pos, pos + 4, [16, 0, 0, 0]); // 块大小 - pos += 4; - header.setRange(pos, pos + 2, [1, 0]); // PCM 格式 - pos += 2; - header.setRange(pos, pos + 2, [numberOfChannels, 0]); // 声道数 - pos += 2; - // 采样率(小端) - header.setRange(pos, pos + 4, [ - sampleRate & 0xff, - (sampleRate >> 8) & 0xff, - (sampleRate >> 16) & 0xff, - (sampleRate >> 24) & 0xff, - ]); - pos += 4; - // 字节率(小端) - header.setRange(pos, pos + 4, [ - byteRate & 0xff, - (byteRate >> 8) & 0xff, - (byteRate >> 16) & 0xff, - (byteRate >> 24) & 0xff, - ]); - pos += 4; - // 块对齐 - header.setRange(pos, pos + 2, [blockAlign, 0]); - pos += 2; - // 位深度 - header.setRange(pos, pos + 2, [bitsPerSample, 0]); - pos += 2; - // data 子块 - header.setRange(pos, pos + 4, [0x64, 0x61, 0x74, 0x61]); // 'data' - pos += 4; - // 数据大小(小端) - header.setRange(pos, pos + 4, [ - dataLength & 0xff, - (dataLength >> 8) & 0xff, - (dataLength >> 16) & 0xff, - (dataLength >> 24) & 0xff, - ]); - return header; - } -} diff --git a/lib/modules/meeting/model/meeting_details_model.dart b/lib/modules/meeting/model/meeting_details_model.dart index fa7e08f97..5e0a83a75 100644 --- a/lib/modules/meeting/model/meeting_details_model.dart +++ b/lib/modules/meeting/model/meeting_details_model.dart @@ -3,25 +3,37 @@ class MeetingDetailsModel { late int id; late int meetingid; - late String audio; - late String content; - late String aisummary; + late String address; + late String personnel; + late String taskid; + late int tasktype; + late String overview; + late String summary; + late String mindmap; factory MeetingDetailsModel.fromJson(Map map) => MeetingDetailsModel() ..id = map["id"] ..meetingid = map["meetingid"] - ..audio = map["audio"] - ..content = map["content"] - ..aisummary = map["aisummary"]; + ..address = map["address"] + ..personnel = map["personnel"] + ..taskid = map["taskid"] + ..tasktype = map["tasktype"] + ..overview = map["overview"] + ..summary = map["summary"] + ..mindmap = map["mindmap"]; Map toJson() { final data = {}; data['id'] = id; data['meetingid'] = meetingid; - data['audio'] = audio; - data['content'] = content; - data['aisummary'] = aisummary; + data['address'] = address; + data['personnel'] = personnel; + data['taskid'] = taskid; + data['tasktype'] = tasktype; + data['overview'] = overview; + data['summary'] = summary; + data['mindmap'] = mindmap; return data; } } diff --git a/lib/modules/meeting/model/meeting_model.dart b/lib/modules/meeting/model/meeting_model.dart index 36f9d3940..b3b2a7869 100644 --- a/lib/modules/meeting/model/meeting_model.dart +++ b/lib/modules/meeting/model/meeting_model.dart @@ -3,32 +3,29 @@ class MeetingModel { late int id; late String title; - late String content; + late String type; late int seconds; - late String address; - late String longitude; - late String latitude; + late String filepath; + late String audiourl; late int creationtime; factory MeetingModel.fromJson(Map map) => MeetingModel() ..id = map["id"] ..title = map["title"] - ..content = map["content"] + ..type = map["type"] ..seconds = map["seconds"] - ..address = map["address"] - ..longitude = map["longitude"] - ..latitude = map["latitude"] + ..filepath = map["filepath"] + ..audiourl = map["audiourl"] ..creationtime = map["creationtime"]; Map toJson() { final data = {}; data['id'] = id; data['title'] = title; - data['content'] = content; + data['type'] = type; data['seconds'] = seconds; - data['address'] = address; - data['longitude'] = longitude; - data['latitude'] = latitude; + data['filepath'] = filepath; + data['audiourl'] = audiourl; data['creationtime'] = creationtime; return data; } diff --git a/lib/modules/meeting/views/meeting_details_view.dart b/lib/modules/meeting/views/meeting_details_view.dart index 52d33cdf1..4e974e27d 100644 --- a/lib/modules/meeting/views/meeting_details_view.dart +++ b/lib/modules/meeting/views/meeting_details_view.dart @@ -1,12 +1,15 @@ +import 'package:audio_waveforms/audio_waveforms.dart'; import 'package:common_utils/common_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; -import 'package:lottie/lottie.dart'; import '../../../core/theme/app_colors.dart'; -import '../../../routes/app_routes.dart'; import '../controllers/meeting_details_controller.dart'; +import 'mind_map_tab.dart'; +import 'overview_tab.dart'; +import 'speech_tab.dart'; +import 'summary_tab.dart'; class MeetingDetailsView extends GetView { const MeetingDetailsView({super.key}); @@ -15,313 +18,356 @@ class MeetingDetailsView extends GetView { Widget build(BuildContext context) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; - return Scaffold( - backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], - appBar: _buildAppBar(isDarkMode), - body: Obx(() => !controller.isLoading.value - ? _buildBody(isDarkMode) - : const SizedBox()), + return Obx( + () => Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + appBar: _buildAppBar(isDarkMode), + body: NestedScrollView( + controller: controller.scrollController, + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverAppBar( + automaticallyImplyLeading: false, + expandedHeight: 210.w, + backgroundColor: Colors.transparent, + flexibleSpace: FlexibleSpaceBar( + collapseMode: CollapseMode.pin, + background: Align( + alignment: Alignment.topCenter, + child: Container( + height: 200.w, + decoration: BoxDecoration( + color: Colors.grey[200]!, + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(20.r), + ), + ), + child: Column( + children: [ + Container( + margin: EdgeInsets.only(bottom: 5.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8.r), + ), + child: AudioFileWaveforms( + size: Size(350.w, 120.w), + playerController: controller.playerController, + playerWaveStyle: PlayerWaveStyle( + fixedWaveColor: Colors.grey[300]!, + liveWaveColor: Colors.red, + seekLineColor: Colors.red, + scaleFactor: 600, + ), + ), + ), + SizedBox( + width: 350.w, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + StreamBuilder( + stream: controller.playerController + .onCurrentDurationChanged, + builder: (context, snapshot) { + final position = snapshot.data ?? 0; + return Text( + DateUtil.formatDateMs( + position, + format: 'HH:mm:ss', + isUtc: true, + ), + style: TextStyle( + color: isDarkMode + ? Colors.grey[400] + : Colors.grey[500], + fontSize: 12.sp, + ), + ); + }, + ), + Obx( + () => Text( + DateUtil.formatDateMs( + controller.durationMax.value, + format: 'HH:mm:ss', + isUtc: true, + ), + style: TextStyle( + color: isDarkMode + ? Colors.grey[400] + : Colors.grey[500], + fontSize: 12.sp, + ), + ), + ), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Icon( + Icons.replay_10, + color: Colors.black, + size: 30.w, + ), + StreamBuilder( + stream: controller + .playerController.onPlayerStateChanged, + builder: (context, snapshot) { + bool isPlay = + snapshot.data?.isPlaying ?? false; + return GestureDetector( + onTap: () { + if (isPlay) { + controller.playerController + .pausePlayer(); + } else { + controller.playerController + .startPlayer(); + } + }, + child: Icon( + isPlay + ? Icons.pause_circle + : Icons.play_circle, + color: Colors.black, + size: 48.w, + ), + ); + }, + ), + Icon( + Icons.forward_10, + color: Colors.black, + size: 30.w, + ), + ], + ), + ], + ), + ), + ), + ), + ), + ]; + }, + body: _buildBody(context, isDarkMode), + ), + ), ); } // 构建应用栏 PreferredSizeWidget _buildAppBar(bool isDarkMode) { return AppBar( - backgroundColor: isDarkMode ? Colors.black : Colors.white, + backgroundColor: + controller.isTop.value ? Colors.grey[50] : Colors.grey[200], + surfaceTintColor: Colors.transparent, elevation: 0.5, - leading: IconButton( - icon: Icon( - Icons.arrow_back_ios, - color: isDarkMode ? Colors.white : Colors.black, - size: 20.sp, - ), - onPressed: () => Get.back(), - ), - title: Text( - 'recordingDetails'.tr, //录音详情 - style: TextStyle( - fontSize: 17.sp, - fontWeight: FontWeight.w600, - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - centerTitle: true, - ); - } - - // 构建主体内容 - Widget _buildBody(bool isDarkMode) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( + leading: controller.isSpeakerText.value + ? GestureDetector( + onTap: () { + controller.isSpeakerText.value = false; + controller.editSpeakerTextId.value = 0; + controller.editSpeakerTextList = []; + }, + child: Center( child: Text( - controller.meetingData.value.title, + 'cancel'.tr, style: TextStyle( color: isDarkMode ? Colors.white : Colors.black87, - fontSize: 17.sp, + fontSize: 16.sp, ), ), ), - 10.horizontalSpace, - Text( - DateUtil.formatDateMs( - controller.meetingData.value.creationtime, - format: DateFormats.y_mo_d_h_m, - ), - style: TextStyle( - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], - fontSize: 14.sp, - ), + ) + : IconButton( + icon: Icon( + Icons.arrow_back_ios, + color: isDarkMode ? Colors.white : Colors.black, + size: 20.sp, ), - ], - ), - Padding( - padding: EdgeInsets.symmetric(vertical: 5.w), - child: Row( - children: [ - Icon( - Icons.location_on, - size: 16.w, - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], - ), - Expanded( - child: Text( - controller.meetingData.value.address, - style: TextStyle( - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], - fontSize: 14.sp, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - _audioWave(isDarkMode), - _recordContent(isDarkMode), - _bottomButton(isDarkMode), - ], - ), - ); - } - - // 音波 - Widget _audioWave(bool isDarkMode) { - return Container( - padding: EdgeInsets.only(bottom: 10.w), - margin: EdgeInsets.symmetric(vertical: 5.w), - decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, - borderRadius: BorderRadius.circular(12.w), - boxShadow: [ - BoxShadow( - color: isDarkMode - ? Colors.black.withOpacity(0.2) - : Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - children: [ - SizedBox( - height: 70.w, - child: Row( - children: [ - Expanded( - child: Lottie.asset( - 'assets/lottie/audio_wave.json', - controller: controller.lottieController, - fit: BoxFit.fill, - onLoaded: (composition) { - controller.lottieController.duration = - composition.duration; - }, - ), - ), - Expanded( - child: Lottie.asset( - 'assets/lottie/audio_wave.json', - controller: controller.lottieController, - fit: BoxFit.fill, - ), - ), - Expanded( - child: Lottie.asset( - 'assets/lottie/audio_wave.json', - controller: controller.lottieController, - fit: BoxFit.fill, - ), - ), - ], + onPressed: () => Get.back(), ), + title: Opacity( + opacity: controller.isTop.value ? 1.0 : 0.0, + child: Container( + width: 170.w, + padding: EdgeInsets.symmetric(horizontal: 2.w, vertical: 3.w), + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(30.r), ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, + child: Row( children: [ - StreamBuilder( - stream: controller.audioPlayer.positionStream, + StreamBuilder( + stream: controller.playerController.onPlayerStateChanged, builder: (context, snapshot) { - final position = snapshot.data ?? Duration.zero; - return Text( - DateUtil.formatDateMs( - position.inMilliseconds, - format: 'HH:mm:ss', - isUtc: true, - ), - style: TextStyle( - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], - fontSize: 12.sp, + bool isPlay = snapshot.data?.isPlaying ?? false; + return GestureDetector( + onTap: () { + if (isPlay) { + controller.playerController.pausePlayer(); + } else { + controller.playerController.startPlayer(); + } + }, + child: Icon( + isPlay ? Icons.pause_circle : Icons.play_circle, + color: Colors.black, + size: 30.w, ), ); }, ), - GestureDetector( - onTap: controller.start, - child: Container( - width: 30.w, - height: 30.w, - decoration: BoxDecoration( - color: isDarkMode ? AppColors.primary : Colors.blue, - borderRadius: BorderRadius.circular( - 12.w, - ), - ), - child: Icon( - controller.isPlay.value ? Icons.pause : Icons.play_arrow, - color: Colors.white, - size: 25.w, - ), + 4.horizontalSpace, + AudioFileWaveforms( + size: Size(130.w, 30.w), + playerController: controller.playerController, + playerWaveStyle: PlayerWaveStyle( + fixedWaveColor: Colors.grey[400]!, + liveWaveColor: Colors.red, + seekLineColor: Colors.red, + scaleFactor: 100, ), ), - StreamBuilder( - stream: controller.audioPlayer.durationStream, - builder: (context, snapshot) { - final duration = snapshot.data ?? Duration.zero; - controller.seconds = duration.inSeconds; - return Text( - DateUtil.formatDateMs( - duration.inMilliseconds, - format: 'HH:mm:ss', - isUtc: true, - ), - style: TextStyle( - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], - fontSize: 12.sp, - ), - ); - }, - ), ], ), - ], + ), ), + centerTitle: true, + actions: [ + if (controller.isSpeakerText.value) + GestureDetector( + onTap: controller.editSpeakerText, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 12.w, + vertical: 6.w, + ), + child: Text( + 'save'.tr, + style: TextStyle( + color: isDarkMode ? AppColors.primary : Colors.blue, + fontSize: 16.sp, + ), + ), + ), + ), + if (!controller.isSpeakerText.value) + IconButton( + icon: Icon( + Icons.ios_share, + color: isDarkMode ? Colors.white : Colors.black, + size: 22.sp, + ), + onPressed: () {}, + ), + if (!controller.isSpeakerText.value) + IconButton( + icon: Container( + width: 22.w, + height: 22.w, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20.r), + border: Border.all( + color: isDarkMode ? Colors.white : Colors.black, + width: 2, + ), + ), + child: Icon( + Icons.more_horiz, + color: isDarkMode ? Colors.white : Colors.black, + size: 16.sp, + ), + ), + onPressed: () {}, + ), + ], ); } - // 记录内容 - Widget _recordContent(bool isDarkMode) { - return Expanded( - child: Container( - width: double.infinity, - padding: EdgeInsets.all(10.w), - margin: EdgeInsets.symmetric(vertical: 5.w), - decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, - borderRadius: BorderRadius.circular(12.w), - boxShadow: [ - BoxShadow( - color: isDarkMode - ? Colors.black.withOpacity(0.2) - : Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), + // 构建主体内容 + Widget _buildBody(BuildContext context, bool isDarkMode) { + return Column( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 8.w), + margin: EdgeInsets.symmetric(vertical: 10.w), + child: Obx( + () => Row( + children: [ + _iconItem(Icons.summarize, '概述', 0), + _iconItem(Icons.message, '转写', 1), + _iconItem(Icons.article, '总结', 2), + _iconItem(Icons.lan, '思维导图', 3), + ], ), - ], + ), ), - child: DefaultTabController( - length: 2, - child: Column( - children: [ - SizedBox( - height: 40.w, - child: TabBar( - indicatorSize: TabBarIndicatorSize.tab, - indicatorColor: isDarkMode ? AppColors.primary : Colors.blue, - labelColor: isDarkMode ? AppColors.primary : Colors.blue, - labelStyle: TextStyle( - color: isDarkMode - ? Colors.white - : const Color.fromARGB(221, 197, 196, 196), - fontSize: 14.sp, - ), - tabs: const [ - Tab(text: 'AI总结'), - Tab(text: '会议原文'), - ], - ), - ), - 10.verticalSpace, - Expanded( - child: TabBarView( - children: [ - SingleChildScrollView( - child: Text( - controller.dataDetails.value.aisummary, - ), - ), - SingleChildScrollView( - child: Text( - controller.dataDetails.value.content, - ), - ), - ], - ), - ), - ], + Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: TabBarView( + controller: controller.tabController, + physics: const NeverScrollableScrollPhysics(), + children: const [ + OverviewTab(), + SpeechTab(), + SummaryTab(), + MindMapTab(), + ], + ), ), ), - ), + ], ); } - // 底部按钮 - Widget _bottomButton(bool isDarkMode) { - return Container( - padding: EdgeInsets.only(top: 20.w, bottom: 10.w), - alignment: Alignment.center, + Widget _iconItem(IconData icon, String title, int index) { + bool isActive = controller.currentIndex.value == index; + + return Expanded( + flex: isActive ? 1 : 0, child: GestureDetector( onTap: () { - Get.toNamed(Routes.meetingEdit, arguments: { - 'id': controller.id, - }); + if (isActive) return; + controller.tabController.index = index; + controller.isSpeakerText.value = false; + controller.editSpeakerTextId.value = 0; + controller.editSpeakerTextList = []; }, child: Container( - width: 130.w, - height: 48.w, + height: 45.w, + padding: EdgeInsets.symmetric(horizontal: 18.w), + margin: EdgeInsets.symmetric(horizontal: 4.w), decoration: BoxDecoration( - color: isDarkMode ? AppColors.primary : Colors.blue, + color: isActive ? Colors.black : Colors.grey[200], borderRadius: BorderRadius.circular(12.w), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( - Icons.edit_calendar, - color: Colors.white, - size: 20.w, - ), - 5.horizontalSpace, - Text( - '编辑', - style: TextStyle(color: Colors.white, fontSize: 16.sp), + icon, + color: isActive ? Colors.white : Colors.grey[600], + size: 20.sp, ), + if (isActive) + Padding( + padding: EdgeInsets.only(left: 5.w), + child: Text( + title, + style: TextStyle( + color: Colors.white, + fontSize: 16.sp, + ), + ), + ), ], ), ), diff --git a/lib/modules/meeting/views/meeting_edit_view.dart b/lib/modules/meeting/views/meeting_edit_view.dart deleted file mode 100644 index 6bcd32988..000000000 --- a/lib/modules/meeting/views/meeting_edit_view.dart +++ /dev/null @@ -1,301 +0,0 @@ -import 'package:common_utils/common_utils.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; -import 'package:lottie/lottie.dart'; - -import '../../../core/theme/app_colors.dart'; -import '../controllers/meeting_edit_controller.dart'; - -class MeetingEditView extends GetView { - const MeetingEditView({super.key}); - - @override - Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return Scaffold( - backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], - appBar: _buildAppBar(isDarkMode), - body: _buildBody(isDarkMode), - ); - } - - // 构建应用栏 - PreferredSizeWidget _buildAppBar(bool isDarkMode) { - return AppBar( - backgroundColor: isDarkMode ? Colors.black : Colors.white, - elevation: 0.5, - leading: IconButton( - icon: Icon( - Icons.arrow_back_ios, - color: isDarkMode ? Colors.white : Colors.black, - size: 20.sp, - ), - onPressed: () => Get.back(), - ), - actions: [ - GestureDetector( - onTap: controller.confirm, - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: 12.w, - vertical: 6.w, - ), - child: Text( - 'save'.tr, - style: TextStyle( - color: isDarkMode ? AppColors.primary : Colors.blue, - fontSize: 18.sp, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ); - } - - // 构建主体内容 - Widget _buildBody(bool isDarkMode) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: 48.w, - alignment: Alignment.center, - decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, - borderRadius: BorderRadius.circular(12.w), - boxShadow: [ - BoxShadow( - color: isDarkMode - ? Colors.black.withOpacity(0.2) - : Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: TextField( - controller: controller.titleController, - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black87, - fontSize: 16.sp, - ), - decoration: InputDecoration( - hintText: 'maxCharacters'.tr, - hintStyle: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black54, - fontSize: 16.sp, - ), - fillColor: Colors.transparent, - border: InputBorder.none, - contentPadding: EdgeInsets.symmetric( - horizontal: 10.w, - ), - ), - ), - ), - Padding( - padding: EdgeInsets.symmetric(vertical: 5.w), - child: Text( - DateUtil.formatDateMs( - controller.creationTime, - format: DateFormats.y_mo_d_h_m, - ), - style: TextStyle( - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], - fontSize: 14.sp, - ), - ), - ), - Row( - children: [ - Icon( - Icons.location_on, - size: 16.w, - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], - ), - Expanded( - child: Obx( - () => Text( - controller.address.value, - style: TextStyle( - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], - fontSize: 14.sp, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ), - ], - ), - _audioWave(isDarkMode), - _recordContent(isDarkMode), - ], - ), - ); - } - - // 音波 - Widget _audioWave(bool isDarkMode) { - return Container( - padding: EdgeInsets.only(bottom: 10.w), - margin: EdgeInsets.symmetric(vertical: 5.w), - decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, - borderRadius: BorderRadius.circular(12.w), - boxShadow: [ - BoxShadow( - color: isDarkMode - ? Colors.black.withOpacity(0.2) - : Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Column( - children: [ - SizedBox( - height: 70.w, - child: Row( - children: [ - Expanded( - child: Lottie.asset( - 'assets/lottie/audio_wave.json', - controller: controller.lottieController, - fit: BoxFit.fill, - onLoaded: (composition) { - controller.lottieController.duration = - composition.duration; - }, - ), - ), - Expanded( - child: Lottie.asset( - 'assets/lottie/audio_wave.json', - controller: controller.lottieController, - fit: BoxFit.fill, - ), - ), - Expanded( - child: Lottie.asset( - 'assets/lottie/audio_wave.json', - controller: controller.lottieController, - fit: BoxFit.fill, - ), - ), - ], - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - StreamBuilder( - stream: controller.audioPlayer.positionStream, - builder: (context, snapshot) { - final position = snapshot.data ?? Duration.zero; - return Text( - DateUtil.formatDateMs( - position.inMilliseconds, - format: 'HH:mm:ss', - isUtc: true, - ), - style: TextStyle( - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], - fontSize: 12.sp, - ), - ); - }, - ), - GestureDetector( - onTap: controller.start, - child: Container( - width: 30.w, - height: 30.w, - decoration: BoxDecoration( - color: isDarkMode ? AppColors.primary : Colors.blue, - borderRadius: BorderRadius.circular( - 12.w, - ), - ), - child: Obx( - () => Icon( - controller.isPlay.value ? Icons.pause : Icons.play_arrow, - color: Colors.white, - size: 25.w, - ), - ), - ), - ), - StreamBuilder( - stream: controller.audioPlayer.durationStream, - builder: (context, snapshot) { - final duration = snapshot.data ?? Duration.zero; - return Text( - DateUtil.formatDateMs( - duration.inMilliseconds, - format: 'HH:mm:ss', - isUtc: true, - ), - style: TextStyle( - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], - fontSize: 12.sp, - ), - ); - }, - ), - ], - ), - ], - ), - ); - } - - // 记录内容 - Widget _recordContent(bool isDarkMode) { - return Expanded( - child: Container( - width: double.infinity, - padding: EdgeInsets.all(10.w), - margin: EdgeInsets.symmetric(vertical: 5.w), - decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, - borderRadius: BorderRadius.circular(12.w), - boxShadow: [ - BoxShadow( - color: isDarkMode - ? Colors.black.withOpacity(0.2) - : Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: TextField( - controller: controller.contentController, - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black87, - fontSize: 14.sp, - ), - maxLines: 50, - decoration: InputDecoration( - hintText: '编辑记录内容', - hintStyle: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black54, - fontSize: 14.sp, - ), - fillColor: Colors.transparent, - border: InputBorder.none, - focusedBorder: InputBorder.none, - contentPadding: EdgeInsets.zero, - ), - ), - ), - ); - } -} diff --git a/lib/modules/meeting/views/meeting_record_view.dart b/lib/modules/meeting/views/meeting_record_view.dart deleted file mode 100644 index f0d820aa2..000000000 --- a/lib/modules/meeting/views/meeting_record_view.dart +++ /dev/null @@ -1,363 +0,0 @@ -import 'package:common_utils/common_utils.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; -import 'package:lottie/lottie.dart'; - -import '../../../core/theme/app_colors.dart'; -import '../../../core/widgets/select_location_map.dart'; -import '../controllers/meeting_record_controller.dart'; - -class MeetingRecordView extends GetView { - const MeetingRecordView({super.key}); - - @override - Widget build(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - - return PopScope( - canPop: false, - onPopInvokedWithResult: (didPop, result) { - if (didPop) return; - _showAlertDialog(isDarkMode); - }, - child: Scaffold( - backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], - appBar: _buildAppBar(isDarkMode), - body: _buildBody(isDarkMode), - ), - ); - } - - // 构建应用栏 - PreferredSizeWidget _buildAppBar(bool isDarkMode) { - return AppBar( - backgroundColor: isDarkMode ? Colors.black : Colors.white, - elevation: 0.5, - leading: IconButton( - icon: Icon( - Icons.arrow_back_ios, - color: isDarkMode ? Colors.white : Colors.black, - size: 20.sp, - ), - onPressed: () { - _showAlertDialog(isDarkMode); - }, - ), - title: Padding( - padding: const EdgeInsets.only(right: 56), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Obx( - () => Icon( - Icons.keyboard_voice, - color: isDarkMode - ? controller.seconds.value > 0 - ? AppColors.primary - : Colors.white - : controller.seconds.value > 0 - ? Colors.blue - : Colors.black, - ), - ), - 5.horizontalSpace, - Obx( - () => Text( - DateUtil.formatDateMs( - controller.seconds.value * 1000, - format: 'HH:mm:ss', - isUtc: true, - ), - style: TextStyle( - fontSize: 17.sp, - fontWeight: FontWeight.w600, - color: isDarkMode - ? controller.seconds.value > 0 - ? AppColors.primary - : Colors.white - : controller.seconds.value > 0 - ? Colors.blue - : Colors.black, - ), - ), - ), - ], - ), - ), - centerTitle: true, - ); - } - - // 构建主体内容 - Widget _buildBody(bool isDarkMode) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - 10.verticalSpace, - Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w), - child: Row( - children: [ - Expanded( - child: Text( - controller.title, - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black87, - fontSize: 17.sp, - ), - ), - ), - 10.horizontalSpace, - Text( - DateUtil.formatDateMs( - controller.creationTime, - format: DateFormats.y_mo_d_h_m, - ), - style: TextStyle( - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], - fontSize: 14.sp, - ), - ), - ], - ), - ), - GestureDetector( - onTap: () { - Get.to(() => const SelectLocationMap()); - }, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w), - child: Row( - children: [ - Icon( - Icons.location_on, - size: 16.w, - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], - ), - Expanded( - child: Obx( - () => Text( - controller.address.value, - style: TextStyle( - color: isDarkMode ? Colors.grey[400] : Colors.grey[500], - fontSize: 14.sp, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ), - ], - ), - ), - ), - _recordContent(isDarkMode), - _bottomButton(isDarkMode), - ], - ); - } - - // 记录内容 - Widget _recordContent(bool isDarkMode) { - return Expanded( - child: Container( - width: double.infinity, - padding: EdgeInsets.all(10.w), - margin: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w), - decoration: BoxDecoration( - color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, - borderRadius: BorderRadius.circular(12.w), - boxShadow: [ - BoxShadow( - color: isDarkMode - ? Colors.black.withOpacity(0.2) - : Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 2), - ), - ], - ), - child: Align( - alignment: Alignment.topLeft, - child: SingleChildScrollView( - reverse: true, - child: Obx( - () => Text( - '${controller.finalContent.value}${controller.intermediateContent.value}', - ), - ), - ), - ), - ), - ); - } - - // 底部按钮 - Widget _bottomButton(bool isDarkMode) { - return Container( - margin: EdgeInsets.only(top: 10.h), - decoration: BoxDecoration( - color: isDarkMode ? AppColors.cardBackground : Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 3, - offset: const Offset(0, -1), - ), - ], - ), - child: SafeArea( - child: SizedBox( - height: 80.h, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // 语音录制按钮 - Container( - width: 60.r, - height: 60.r, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.white, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 8, - spreadRadius: 1, - ), - ], - ), - child: Center( - child: Obx( - () => IconButton( - icon: Icon( - controller.isRecording.value - ? Icons.mic - : Icons.mic_none, - ), - color: controller.isRecording.value - ? AppColors.primary - : Colors.grey.shade400, - iconSize: 28.r, - padding: EdgeInsets.zero, - onPressed: controller.recorderHandle, - ), - ), - ), - ), - 10.horizontalSpace, - Expanded( - child: Lottie.asset( - 'assets/lottie/audio_wave.json', - controller: controller.lottieController, - fit: BoxFit.fill, - onLoaded: (composition) { - controller.lottieController.duration = - composition.duration; - }, - ), - ), - Expanded( - child: Lottie.asset( - 'assets/lottie/audio_wave.json', - controller: controller.lottieController, - fit: BoxFit.fill, - onLoaded: (composition) { - controller.lottieController.duration = - composition.duration; - }, - ), - ), - 10.horizontalSpace, - // 关闭按钮 - Container( - width: 60.r, - height: 60.r, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.white, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 8, - spreadRadius: 1, - ), - ], - ), - child: Center( - child: IconButton( - icon: Icon( - Icons.close, - color: isDarkMode ? Colors.white : Colors.grey.shade600, - size: 28.r, - ), - padding: EdgeInsets.zero, - onPressed: () { - _showAlertDialog(isDarkMode); - }, - ), - ), - ), - ], - ), - ), - ), - ), - ); - } - - // 显示对话框 - void _showAlertDialog(bool isDarkMode) { - if (controller.seconds.value > 0) { - 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.white70 : Colors.black54, - ), - ), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: Text( - 'cancel'.tr, // 取消 - style: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - ), - ), - TextButton( - onPressed: () { - Get.back(); - controller.complete(); - }, - child: Text( - 'confirm'.tr, // 确定 - style: TextStyle( - color: isDarkMode ? Colors.red[300] : Colors.red[600], - ), - ), - ), - ], - ), - ); - } else { - Get.back(); - } - } -} diff --git a/lib/modules/meeting/views/meeting_view.dart b/lib/modules/meeting/views/meeting_view.dart index 858ece898..4aa03a9a5 100644 --- a/lib/modules/meeting/views/meeting_view.dart +++ b/lib/modules/meeting/views/meeting_view.dart @@ -2,6 +2,7 @@ import 'package:common_utils/common_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:haptic_feedback/haptic_feedback.dart'; import '../../../core/theme/app_colors.dart'; import '../../../routes/app_routes.dart'; @@ -19,6 +20,10 @@ class MeetingView extends GetView { backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], appBar: _buildAppBar(isDarkMode), body: _buildBody(context, isDarkMode), + floatingActionButton: FloatingActionButton( + onPressed: controller.addAudio, + child: const Icon(Icons.add), + ), ); } @@ -49,50 +54,15 @@ class MeetingView extends GetView { // 构建主体内容 Widget _buildBody(BuildContext context, bool isDarkMode) { - return Stack( - children: [ - Obx( - () => ListView.builder( - itemCount: controller.dataList.length, - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w), - itemBuilder: (BuildContext context, int index) { - MeetingModel data = - MeetingModel.fromJson(controller.dataList[index]); - return _item(context, isDarkMode, data); - }, - ), - ), - Align( - alignment: Alignment.bottomCenter, - child: GestureDetector( - onTap: () { - _bottomSheet(context, isDarkMode); - }, - child: Container( - //width: 130.w, - height: 48.w, - margin: EdgeInsets.symmetric(vertical: 20.w), - padding: EdgeInsets.symmetric(horizontal: 14.w), - decoration: BoxDecoration( - color: isDarkMode ? AppColors.primary : Colors.blue, - borderRadius: BorderRadius.circular(12.w), - ), - child: Row( - mainAxisSize: MainAxisSize.min, // 添加这行使Row宽度自适应内容 - //mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.keyboard_voice, color: Colors.white), - 5.horizontalSpace, - Text( - 'startMeeting'.tr,// 开始会议 - style: TextStyle(color: Colors.white, fontSize: 16.sp), - ), - ], - ), - ), - ), - ), - ], + return Obx( + () => ListView.builder( + itemCount: controller.dataList.length, + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w), + itemBuilder: (BuildContext context, int index) { + MeetingModel data = MeetingModel.fromJson(controller.dataList[index]); + return _item(context, isDarkMode, data); + }, + ), ); } @@ -103,52 +73,11 @@ class MeetingView extends GetView { 'id': data.id, }); }, - onLongPressStart: (details) { - RenderBox? renderBox = - Overlay.of(context).context.findRenderObject() as RenderBox; - final RelativeRect position = RelativeRect.fromRect( - Rect.fromLTRB( - details.globalPosition.dx, - details.globalPosition.dy, - details.globalPosition.dx + 110, // 菜单显示位置X轴坐标 - details.globalPosition.dy - 40, // 菜单显示位置Y轴坐标 - ), - Offset.zero & renderBox.size, - ); - - showMenu( - context: context, - position: position, - color: isDarkMode ? AppColors.cardBackground : Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.r), - ), - items: [ - _popupMenuEntry( - Icon( - Icons.edit_calendar, - color: isDarkMode ? Colors.white : Colors.black87, - ), - 'edit'.tr,// 编辑 - isDarkMode ? Colors.white : Colors.black87, - () { - controller.titleController.text = data.title; - _bottomSheet(context, isDarkMode, id: data.id); - }, - ), - _popupMenuEntry( - const Icon(Icons.delete_outline, color: Colors.red), - 'delete'.tr,// 删除 - isDarkMode ? Colors.white : Colors.black87, - () { - controller.deleteMeeting(data.id); - }, - ), - ], - ); + onLongPressStart: (LongPressStartDetails details) { + _onLongPressItem(context, isDarkMode, details, data); }, child: Container( - padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 8.w), + padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w), margin: EdgeInsets.symmetric(vertical: 5.w), decoration: BoxDecoration( color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, @@ -164,89 +93,67 @@ class MeetingView extends GetView { ], ), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Expanded( - child: Text( - data.title, + Text( + data.title, + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + fontSize: 18.sp, + ), + overflow: TextOverflow.ellipsis, + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 5.w), + child: Row( + children: [ + Icon( + Icons.calendar_month, + size: 14.w, + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + ), + 2.horizontalSpace, + Text( + DateUtil.formatDateMs( + data.creationtime, + format: DateFormats.y_mo_d_h_m, + ), style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black87, - fontSize: 16.sp, + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 12.sp, ), - overflow: TextOverflow.ellipsis, ), - ), - 10.horizontalSpace, - Text( - DateUtil.formatDateMs( - data.creationtime, - format: DateFormats.y_mo_d_h_m, + 20.horizontalSpace, + Icon( + Icons.access_time, + size: 14.w, + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], ), - style: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black54, - fontSize: 12.sp, - ), - ), - ], - ), - Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - data.content.isNotEmpty ? data.content : 'noContent'.tr,// 无内容 - style: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black54, - fontSize: 12.sp, - ), - overflow: TextOverflow.ellipsis, - ), - Row( - children: [ - Icon( - Icons.location_on, - size: 14.w, - color: isDarkMode ? Colors.white70 : Colors.black54, - ), - Expanded( - child: Text( - data.address, - style: TextStyle( - color: isDarkMode - ? Colors.white70 - : Colors.black54, - fontSize: 12.sp, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ], - ), - ), - Container( - width: 76.w, - height: 28.w, - margin: EdgeInsets.only(left: 10.w), - alignment: Alignment.center, - decoration: BoxDecoration( - color: isDarkMode ? AppColors.primary : Colors.blue, - borderRadius: BorderRadius.circular(28.w), - ), - child: Text( - DateUtil.formatDateMs( - data.seconds * 1000, - format: 'HH:mm:ss', - isUtc: true, + 2.horizontalSpace, + Text( + '${data.seconds}s', + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 12.sp, ), - style: TextStyle(color: Colors.white, fontSize: 14.sp), ), + ], + ), + ), + Container( + padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.w), + margin: EdgeInsets.only(top: 5.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.grey[100], + borderRadius: BorderRadius.circular(4.w), + ), + child: Text( + data.type, + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + fontSize: 10.sp, ), - ], + ), ), ], ), @@ -254,6 +161,58 @@ class MeetingView extends GetView { ); } + void _onLongPressItem(BuildContext context, bool isDarkMode, + LongPressStartDetails details, MeetingModel data) { + Haptics.vibrate(HapticsType.success); + RenderBox? renderBox = + Overlay.of(context).context.findRenderObject() as RenderBox; + final RelativeRect position = RelativeRect.fromRect( + Rect.fromLTRB( + details.globalPosition.dx, + details.globalPosition.dy, + details.globalPosition.dx + 110, // 菜单显示位置X轴坐标 + details.globalPosition.dy - 40, // 菜单显示位置Y轴坐标 + ), + Offset.zero & renderBox.size, + ); + + showMenu( + context: context, + position: position, + color: isDarkMode ? AppColors.cardBackground : Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + items: [ + _popupMenuEntry( + Icon( + Icons.edit_calendar, + size: 20.w, + color: isDarkMode ? Colors.white : Colors.black87, + ), + 'edit'.tr, // 编辑 + isDarkMode ? Colors.white : Colors.black87, + () { + controller.titleController.text = data.title; + _bottomSheet(context, isDarkMode, data.id); + }, + ), + _popupMenuEntry( + Icon( + Icons.delete_outline, + size: 20.w, + color: isDarkMode ? Colors.red[700] : Colors.red, + ), + 'delete'.tr, // 删除 + isDarkMode ? Colors.red[700]! : Colors.red, + () { + controller.deleteMeeting(data.id, data.filepath); + }, + ), + ], + ); + } + PopupMenuEntry _popupMenuEntry( Icon icon, String title, @@ -263,14 +222,17 @@ class MeetingView extends GetView { return PopupMenuItem( onTap: onTap, child: SizedBox( - width: 120.w, + width: 140.w, child: Row( children: [ icon, 8.horizontalSpace, Text( title, - style: TextStyle(color: color), + style: TextStyle( + color: color, + fontSize: 14.sp, + ), ), ], ), @@ -280,9 +242,9 @@ class MeetingView extends GetView { void _bottomSheet( BuildContext context, - bool isDarkMode, { - int? id, - }) { + bool isDarkMode, + int id, + ) { showModalBottomSheet( context: context, isScrollControlled: true, @@ -298,7 +260,7 @@ class MeetingView extends GetView { mainAxisSize: MainAxisSize.min, children: [ Text( - id != null ? 'editMeetingTitle'.tr : 'meetingTitle'.tr, // 编辑会议标题 + 'editMeetingTitle'.tr, // 编辑会议标题 style: TextStyle( color: isDarkMode ? Colors.white : Colors.black87, fontSize: 16.sp, @@ -363,7 +325,7 @@ class MeetingView extends GetView { borderRadius: BorderRadius.circular(12.w), ), child: Text( - 'cancel'.tr,// 取消 + 'cancel'.tr, // 取消 style: TextStyle( fontSize: 14.sp, color: @@ -374,20 +336,8 @@ class MeetingView extends GetView { ), GestureDetector( onTap: () { - int time = DateTime.now().millisecondsSinceEpoch; String title = controller.titleController.text; - if (id != null) { - controller.editMeetingTitle(id, title); - } else { - if (title.isEmpty) { - title = - '${DateUtil.formatDateMs(time, format: DateFormats.y_mo_d)} ${'meeting'.tr}'; - } - Get.back(); - Get.toNamed(Routes.meetingRecord, arguments: { - 'title': title, - }); - } + controller.editMeetingTitle(id, title); }, child: Container( width: 120.w, @@ -398,7 +348,7 @@ class MeetingView extends GetView { borderRadius: BorderRadius.circular(12.w), ), child: Text( - 'confirm'.tr,// 确认 + 'confirm'.tr, // 确认 style: TextStyle( color: Colors.white, fontSize: 14.sp, @@ -414,8 +364,6 @@ class MeetingView extends GetView { ), ); }, - ).then((value) { - controller.titleController.text = ''; - }); + ); } } diff --git a/lib/modules/meeting/views/mind_map_tab.dart b/lib/modules/meeting/views/mind_map_tab.dart new file mode 100644 index 000000000..a4e783ba9 --- /dev/null +++ b/lib/modules/meeting/views/mind_map_tab.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class MindMapTab extends StatelessWidget { + const MindMapTab({super.key}); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Text(''), + ); + } +} diff --git a/lib/modules/meeting/views/overview_tab.dart b/lib/modules/meeting/views/overview_tab.dart new file mode 100644 index 000000000..7f7b6064d --- /dev/null +++ b/lib/modules/meeting/views/overview_tab.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class OverviewTab extends StatelessWidget { + const OverviewTab({super.key}); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Text(''), + ); + } +} diff --git a/lib/modules/meeting/views/speaker_bottom_sheet.dart b/lib/modules/meeting/views/speaker_bottom_sheet.dart new file mode 100644 index 000000000..2d0bcf665 --- /dev/null +++ b/lib/modules/meeting/views/speaker_bottom_sheet.dart @@ -0,0 +1,202 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +class SpeakerBottomSheet extends StatelessWidget { + const SpeakerBottomSheet({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: EdgeInsets.symmetric(vertical: 10.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '命名当前发言者', + style: TextStyle( + fontSize: 16.sp, + color: Colors.black87, + fontWeight: FontWeight.bold, + ), + ), + GestureDetector( + onTap: () => Get.back(), + child: Container( + width: 26.w, + height: 26.w, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(26.r), + ), + child: Icon( + Icons.close, + size: 18.w, + color: Colors.grey[400], + ), + ), + ) + ], + ), + ), + Divider( + height: 1, + color: Colors.grey[300], + ), + Container( + height: 46.w, + margin: EdgeInsets.symmetric(vertical: 15.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.r), + ), + ), + Container( + constraints: BoxConstraints(maxHeight: 1.sh - 390.w), + child: ListView.builder( + itemCount: 30, + shrinkWrap: true, + itemBuilder: (BuildContext context, int index) { + return Container( + padding: EdgeInsets.only(left: 10.w), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + width: 1, + color: Colors.grey[300]!, + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (index == 0) + Text( + '最近使用过的名称', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey[600]!, + ), + ), + SizedBox( + height: 42.w, + child: Align( + alignment: Alignment.centerLeft, + child: Text( + '龙哥', + style: TextStyle( + fontSize: 14.sp, + color: Colors.black87, + ), + ), + ), + ), + ], + ), + ); + }, + ), + ), + _row('应用到当前段落', false), + _row('应用到这个发言者的所有段落', true, description: '总结将根据模版自动更新'), + Padding( + padding: EdgeInsets.only(top: 10.w, bottom: 20.w), + child: GestureDetector( + onTap: () {}, + child: Container( + height: 48.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(12.r), + ), + child: Text( + '保存', + style: TextStyle( + fontSize: 16.sp, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ); + } + + Widget _row(String name, bool isActive, {String? description}) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 10.w), + child: Row( + children: [ + isActive ? _round() : _circle(), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + style: TextStyle( + fontSize: 14.sp, + color: Colors.black87, + ), + ), + if (description != null) + Text( + description, + style: TextStyle( + fontSize: 10.sp, + color: Colors.grey[500]!, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _circle() { + return Container( + width: 18.w, + height: 18.w, + margin: EdgeInsets.only(right: 6.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(18.r), + border: Border.all( + width: 1.5, + color: Colors.grey[300]!, + ), + ), + ); + } + + Widget _round() { + return Container( + width: 18.w, + height: 18.w, + padding: EdgeInsets.all(2.w), + margin: EdgeInsets.only(right: 6.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(18.r), + border: Border.all( + width: 1.5, + color: Colors.black87, + ), + ), + child: Container( + decoration: BoxDecoration( + color: Colors.black87, + borderRadius: BorderRadius.circular(18.r), + ), + ), + ); + } +} diff --git a/lib/modules/meeting/views/speech_tab.dart b/lib/modules/meeting/views/speech_tab.dart new file mode 100644 index 000000000..d0c58e85c --- /dev/null +++ b/lib/modules/meeting/views/speech_tab.dart @@ -0,0 +1,256 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +import '../controllers/meeting_details_controller.dart'; +import 'speaker_bottom_sheet.dart'; + +class SpeechTab extends GetView { + const SpeechTab({super.key}); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Obx( + () => ListView.builder( + itemCount: controller.textList.length, + padding: EdgeInsets.only(bottom: 80.w), + itemBuilder: (BuildContext context, int index) { + Map item = controller.textList[index]; + TextStyle textStyle = TextStyle( + fontSize: 16.sp, + color: Colors.black87, + letterSpacing: 0, + height: 1.4, + ); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(top: 10.w), + child: Row( + children: [ + if (item['speaker'].isNotEmpty) + Container( + width: 12.w, + height: 12.w, + margin: EdgeInsets.only(right: 2.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(28.r), + border: Border.all( + color: const Color(0xFFFFC107), + width: 3.w, + ), + ), + ), + if (item['speaker'].isNotEmpty) + GestureDetector( + onTap: () { + if (controller.isSpeakerText.value) return; + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + builder: (BuildContext context) { + return const SpeakerBottomSheet(); + }, + ); + }, + child: Padding( + padding: EdgeInsets.only(right: 5.w), + child: Text( + item['speaker'], + style: TextStyle( + color: const Color(0xFFFFC107), + fontSize: 14.sp, + ), + ), + ), + ), + Text( + DateUtil.formatDateMs( + item['starttime'], + format: 'HH:mm:ss', + isUtc: true, + ), + style: TextStyle( + color: Colors.grey, + fontSize: 14.sp, + ), + ), + ], + ), + ), + Obx(() { + if (controller.isSpeakerText.value) { + TextEditingController speakerTextController = + TextEditingController(text: item['content']); + return Obx( + () => Container( + padding: EdgeInsets.symmetric( + horizontal: 14.w, + vertical: 5.w, + ), + decoration: BoxDecoration( + color: + controller.editSpeakerTextId.value == item['id'] + ? Colors.blue[100] + : null, + borderRadius: BorderRadius.circular(8.w), + ), + child: FocusScope( + onFocusChange: (bool hasFocus) { + if (hasFocus) { + controller.editSpeakerTextId.value = item['id']; + controller.editSpeakerTextChange = ''; + } else { + if (speakerTextController.text != + item['content']) { + Map? foundElement = + controller.editSpeakerTextList.firstWhere( + (element) => element['id'] == item['id'], + orElse: () => null, + ); + if (foundElement != null) { + foundElement['content'] = + speakerTextController.text; + } else { + controller.editSpeakerTextList.add({ + 'id': item['id'], + 'content': speakerTextController.text, + }); + } + } + } + }, + child: TextField( + controller: speakerTextController, + style: textStyle, + minLines: 1, + maxLines: 100, + decoration: const InputDecoration( + isDense: true, + fillColor: Colors.transparent, + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + ), + onChanged: (String value) { + controller.editSpeakerTextChange = value; + }, + ), + ), + ), + ); + } else { + return Container( + padding: EdgeInsets.symmetric( + horizontal: 14.w, + vertical: 5.w, + ), + child: Text( + item['content'], + style: textStyle, + ), + ); + } + }), + ], + ); + }, + ), + ), + Obx( + () => Positioned( + bottom: 20.w, + left: 0, + right: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (controller.meetingDetails.value.tasktype == 1 && + !controller.isSpeakerText.value) + _button( + Icons.edit, + '编辑', + () { + controller.isSpeakerText.value = true; + }, + ), + if (controller.meetingDetails.value.tasktype == 1 && + !controller.isSpeakerText.value) + _button( + Icons.supervisor_account, + '命名发言者', + () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + builder: (BuildContext context) { + return const SpeakerBottomSheet(); + }, + ); + }, + ), + if (controller.meetingDetails.value.tasktype != 1) + SizedBox( + width: 150.w, + child: _button( + Icons.auto_awesome, + '生成', + controller.speakerCreate, + ), + ), + ], + ), + ), + ), + ], + ); + } + + Widget _button(IconData icon, String title, Function() onTap) { + return GestureDetector( + onTap: onTap, + child: Container( + height: 42.w, + padding: EdgeInsets.symmetric(horizontal: 15.w), + margin: EdgeInsets.symmetric(horizontal: 4.w), + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(42.w), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + color: Colors.white, + size: 20.sp, + ), + 5.horizontalSpace, + Text( + title, + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/meeting/views/summary_tab.dart b/lib/modules/meeting/views/summary_tab.dart new file mode 100644 index 000000000..073df9df2 --- /dev/null +++ b/lib/modules/meeting/views/summary_tab.dart @@ -0,0 +1,226 @@ +import 'package:common_utils/common_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +import '../controllers/meeting_details_controller.dart'; +import 'speaker_bottom_sheet.dart'; + +class SummaryTab extends GetView { + const SummaryTab({super.key}); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Stack( + children: [ + SingleChildScrollView( + padding: EdgeInsets.only(bottom: 80.w), + child: Obx( + () => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.symmetric(vertical: 10.w), + child: Text( + controller.meetingData.value.title, + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + Row( + children: [ + Container( + padding: + EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.w), + margin: EdgeInsets.only(right: 10.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.grey[200], + borderRadius: BorderRadius.circular(4.w), + ), + child: Text( + controller.meetingData.value.type, + style: TextStyle( + color: + isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 10.sp, + ), + ), + ), + Icon( + Icons.calendar_month, + size: 14.w, + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + ), + 2.horizontalSpace, + Text( + DateUtil.formatDateMs( + controller.meetingData.value.creationtime, + format: DateFormats.y_mo_d_h_m, + ), + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 12.sp, + ), + ), + 10.horizontalSpace, + Icon( + Icons.access_time, + size: 14.w, + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + ), + 2.horizontalSpace, + Text( + '${controller.meetingData.value.seconds}s', + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 12.sp, + ), + ), + ], + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 10.w), + child: Divider( + height: 1, + color: isDarkMode ? Colors.grey[900] : Colors.grey[300], + ), + ), + Container( + padding: EdgeInsets.only(left: 20.w), + margin: EdgeInsets.symmetric(vertical: 10.w), + decoration: BoxDecoration( + border: Border( + left: BorderSide( + width: 2, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[300]!, + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '日期和时间:${DateUtil.formatDateMs( + controller.meetingData.value.creationtime, + format: DateFormats.y_mo_d_h_m, + )}', + style: TextStyle( + color: + isDarkMode ? Colors.grey[400] : Colors.grey[700], + fontSize: 16.sp, + ), + ), + Text( + '地点:${controller.meetingDetails.value.address.isNotEmpty ? controller.meetingDetails.value.address : '[输入地点]'}', + style: TextStyle( + color: + isDarkMode ? Colors.grey[400] : Colors.grey[700], + fontSize: 16.sp, + ), + ), + Text( + '与会人员:${controller.meetingDetails.value.address}', + style: TextStyle( + color: + isDarkMode ? Colors.grey[400] : Colors.grey[700], + fontSize: 16.sp, + ), + ), + ], + ), + ), + Padding( + padding: EdgeInsets.only(top: 10.w), + child: MarkdownBody( + data: controller.meetingDetails.value.summary, + ), + ), + ], + ), + ), + ), + Obx( + () => Positioned( + bottom: 20.w, + left: 0, + right: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (controller.meetingDetails.value.summary.isNotEmpty) + _button( + Icons.supervisor_account, + '编辑发言者', + () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + builder: (BuildContext context) { + return const SpeakerBottomSheet(); + }, + ); + }, + ), + if (controller.meetingDetails.value.summary.isEmpty) + SizedBox( + width: 150.w, + child: _button( + Icons.auto_awesome, + '生成', + controller.summaryCreate, + ), + ), + ], + ), + ), + ), + ], + ); + } + + Widget _button(IconData icon, String title, Function() onTap) { + return GestureDetector( + onTap: onTap, + child: Container( + height: 42.w, + padding: EdgeInsets.symmetric(horizontal: 15.w), + margin: EdgeInsets.symmetric(horizontal: 4.w), + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(42.w), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + color: Colors.white, + size: 20.sp, + ), + 5.horizontalSpace, + Text( + title, + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index fe3576f12..3192a4927 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -7,11 +7,7 @@ import '../modules/explore/views/explore_view.dart'; import '../modules/explore/bindings/explore_binding.dart'; import '../modules/meeting/bindings/meeting_binding.dart'; import '../modules/meeting/bindings/meeting_details_binding.dart'; -import '../modules/meeting/bindings/meeting_edit_binding.dart'; -import '../modules/meeting/bindings/meeting_record_binding.dart'; import '../modules/meeting/views/meeting_details_view.dart'; -import '../modules/meeting/views/meeting_edit_view.dart'; -import '../modules/meeting/views/meeting_record_view.dart'; import '../modules/meeting/views/meeting_view.dart'; import '../modules/profile/views/profile_view.dart'; import '../modules/profile/bindings/profile_binding.dart'; @@ -138,13 +134,6 @@ abstract class AppPages { transition: Transition.rightToLeft, transitionDuration: Duration(milliseconds: 250), ), - GetPage( - name: Routes.meetingRecord, - page: () => const MeetingRecordView(), - binding: MeetingRecordBinding(), - transition: Transition.rightToLeft, - transitionDuration: Duration(milliseconds: 250), - ), GetPage( name: Routes.meetingDetails, page: () => const MeetingDetailsView(), @@ -152,13 +141,6 @@ abstract class AppPages { transition: Transition.rightToLeft, transitionDuration: Duration(milliseconds: 250), ), - GetPage( - name: Routes.meetingEdit, - page: () => const MeetingEditView(), - binding: MeetingEditBinding(), - transition: Transition.rightToLeft, - transitionDuration: Duration(milliseconds: 250), - ), GetPage( name: Routes.timbre, page: () => const TimbreView(), diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 08500199f..d04850623 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -20,9 +20,7 @@ abstract class Routes { // 会议相关 static const meeting = '/meeting'; - static const meetingRecord = '/meeting_record'; // 会议记录 - static const meetingDetails = '/meeting_details'; // 会议详情 - static const meetingEdit = '/meeting_edit'; // 会议编辑 + static const meetingDetails = '/meetingDetails'; // 语言功能 static const translation = '/translation'; // 实时翻译 diff --git a/pubspec.yaml b/pubspec.yaml index 427b3b9c1..37e68bb5b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,6 +36,7 @@ dependencies: package_info_plus: ^8.3.0 file_picker: ^8.0.0+1 haptic_feedback: ^0.5.1+1 + tencentcloud_cos_sdk_plugin: ^1.2.3 window_size: git: From f09ee7eb8db04710faaeb247945ae299d263215a Mon Sep 17 00:00:00 2001 From: wolfplus Date: Tue, 13 May 2025 11:14:30 +0100 Subject: [PATCH 3/7] add --- .../agent/controllers/agent_controller.dart | 249 ++++++++++++----- lib/modules/agent/views/agent_view.dart | 260 +++++++++++++----- lib/modules/agent/views/message_bubble.dart | 4 +- .../controllers/pairing_controller.dart | 2 +- .../agent_service/AgentService.kt | 3 +- .../android/src/main/AndroidManifest.xml | 6 +- .../music_service/MusicService.kt | 2 +- .../open_ai_service/android/build.gradle.kts | 1 + .../open_ai_service/OpenAIService.kt | 4 +- .../open_ai_service/SystemFunctionHandler.kt | 31 +-- 10 files changed, 398 insertions(+), 164 deletions(-) diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 2aef7f7ac..d1b41afd0 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -8,6 +8,9 @@ import 'package:agent_service/agent_service.dart'; import 'package:logger/logger.dart'; import 'package:chat_storage/chat_storage.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:app_settings/app_settings.dart'; class Message { final bool isUser; @@ -51,6 +54,8 @@ class AgentController extends GetxController { final isListening = false.obs; final isSpeaking = false.obs; final isProcessing = false.obs; + // TTS启用状态 + final isTtsEnabled = true.obs; // 图片处理状态 final isImageProcessing = false.obs; @@ -81,6 +86,7 @@ class AgentController extends GetxController { @override void onInit() { super.onInit(); + _loadTtsPreference(); _loadChatHistory(); _subscribeToEvents(); } @@ -95,6 +101,36 @@ class AgentController extends GetxController { super.onClose(); } + // 加载TTS偏好设置 + Future _loadTtsPreference() async { + try { + final storage = GetStorage(); + isTtsEnabled.value = storage.read('agent_tts_enabled') ?? true; + } catch (e) { + // 如果读取失败,使用默认值 + logger.e('加载TTS偏好设置失败: $e'); + isTtsEnabled.value = true; + } + } + + // 切换TTS状态 + Future toggleTts() async { + isTtsEnabled.toggle(); + + // 如果关闭TTS,停止当前语音 + if (!isTtsEnabled.value && isSpeaking.value) { + await AgentService.stopTts(); + } + + // 保存偏好设置 + try { + final storage = GetStorage(); + await storage.write('agent_tts_enabled', isTtsEnabled.value); + } catch (e) { + logger.e('保存TTS偏好设置失败: $e'); + } + } + // 加载聊天历史记录 Future _loadChatHistory() async { try { @@ -134,12 +170,8 @@ class AgentController extends GetxController { _addWelcomeMessage(); } finally { isHistoryLoaded.value = true; - messages.refresh(); - // 在加载完成后直接定位到底部,无需动画 - WidgetsBinding.instance.addPostFrameCallback((_) { - scrollController.jumpTo(scrollController.position.maxScrollExtent); - }); + // 使用reverse:true时不需要手动滚动到底部 } } @@ -199,7 +231,7 @@ class AgentController extends GetxController { )); } messages.refresh(); - _scrollToBottom(); + // 使用reverse:true时通常不需要手动滚动 } break; @@ -229,8 +261,7 @@ class AgentController extends GetxController { isProcessing.value = true; // 标记为新的AI回复 _isNewAssistantResponse = true; - // 滚动到底部 - _scrollToBottom(); + // 使用reverse:true时通常不需要手动滚动 } else if (index >= 0) { // 如果识别结果为空,移除临时的识别消息 messages.removeAt(index); @@ -282,8 +313,7 @@ class AgentController extends GetxController { ); } } - // 滚动到底部 - _scrollToBottom(); + // 使用reverse:true时通常不需要手动滚动 } break; @@ -334,8 +364,7 @@ class AgentController extends GetxController { // 发送此消息到AI处理 isProcessing.value = true; _isNewAssistantResponse = true; - AgentService.processTextInput(messages[index].text, - speakResponse: true); + _handleAutoStop(index); } } break; @@ -351,33 +380,39 @@ class AgentController extends GetxController { final result = event.data['result'] ?? ''; final functionCall = event.data['function_call'] ?? ''; logger.i('Flutter 结束调用 mcp: $result, $functionCall'); - if (result.isNotEmpty) { final parsedResult = jsonDecode(result); - final meta = parsedResult["meta"] ?? ""; - final metaResult = jsonDecode(meta); - - logger.i('Flutter 结束调用 mcp metaResult: $metaResult'); - // 判断是否为新的回复或响应ID是否改变 - if (_isNewAssistantResponse || _currentResponseId != responseId) { - // 添加新消息 - messages.add(Message(isUser: false, text: "", meta: metaResult)); - _isNewAssistantResponse = false; - _currentResponseId = responseId; - } else { - // 更新最后一条消息 - final lastIndex = messages.length - 1; - if (lastIndex >= 0 && !messages[lastIndex].isUser) { - messages[lastIndex] = Message( - isUser: false, - text: messages[lastIndex].text, - meta: meta, - timestamp: messages[lastIndex].timestamp, - ); + final meta = parsedResult["meta"]; + Map metaResult = {}; + + if (meta != null && meta is String && meta.isNotEmpty) { + try { + metaResult = jsonDecode(meta); + } catch (e) { + print('解析meta数据失败: $e'); + } + } + if(metaResult.isNotEmpty){ + + // 判断是否为新的回复或响应ID是否改变 + if (_isNewAssistantResponse || _currentResponseId != responseId) { + // 添加新消息 + messages.add(Message(isUser: false, text: "", meta: metaResult)); + _isNewAssistantResponse = false; + _currentResponseId = responseId; + } else { + // 更新最后一条消息 + final lastIndex = messages.length - 1; + if (lastIndex >= 0 && !messages[lastIndex].isUser) { + messages[lastIndex] = Message( + isUser: false, + text: messages[lastIndex].text, + meta: meta, + timestamp: messages[lastIndex].timestamp, + ); + } } } - // 滚动到底部 - _scrollToBottom(); } break; default: @@ -388,11 +423,13 @@ class AgentController extends GetxController { // 滚动到底部 void _scrollToBottom() { + // 使用reverse:true时,此方法可大部分场景下不再需要 + // 保留此方法但简化实现,仅用于特殊情况下的手动滚动 WidgetsBinding.instance.addPostFrameCallback((_) { if (scrollController.hasClients) { try { scrollController.animateTo( - scrollController.position.maxScrollExtent, + 0, // 使用reverse:true时,0表示列表底部 duration: Duration(milliseconds: 300), curve: Curves.easeOut, ); @@ -423,14 +460,14 @@ class AgentController extends GetxController { messages.add(message); textController.clear(); - // 滚动到底部 - _scrollToBottom(); + // 使用reverse:true时通常不需要手动滚动 try { isProcessing.value = true; // 标记为新的AI回复 _isNewAssistantResponse = true; - await AgentService.processTextInput(text, speakResponse: true); + // 根据TTS状态决定是否朗读 + await AgentService.processTextInput(text, speakResponse: isTtsEnabled.value); } catch (e) { logger.e('发送文本消息失败: $e'); isProcessing.value = false; @@ -459,8 +496,7 @@ class AgentController extends GetxController { // 清空文本输入 textController.clear(); - // 滚动到底部 - _scrollToBottom(); + // 使用reverse:true时通常不需要手动滚动 try { isProcessing.value = true; @@ -468,11 +504,11 @@ class AgentController extends GetxController { // 标记为新的AI回复 _isNewAssistantResponse = true; - // 调用Agent Service处理图片 + // 调用Agent Service处理图片,考虑TTS状态 await AgentService.processImageInput( imagePath, text: text ?? '', - speakResponse: true, + speakResponse: isTtsEnabled.value, ); } catch (e) { logger.e('处理图片失败: $e'); @@ -481,10 +517,108 @@ class AgentController extends GetxController { } } + // 切换输入模式 + void toggleInputMode() async { + // 如果当前在图片输入模式,先清除 + if (isImageInputActive.value) { + clearImageInput(); + return; + } + + // 如果要切换到语音模式,需要先检查麦克风权限 + if (isTextInputMode.value) { + final hasPermission = await _checkMicrophonePermission(); + if (!hasPermission) { + return; // 权限未获取,保持文本模式 + } + } + + isTextInputMode.toggle(); + + // 切换到语音模式时,直接开始语音输入 + if (!isTextInputMode.value) { + startVoiceInput(); + } + // 切换到文本模式时,直接停止语音输入 + else { + stopVoiceInput(); + } + } + + // 检查麦克风权限 + Future _checkMicrophonePermission() async { + try { + PermissionStatus status = await Permission.microphone.status; + + // 如果已授权,直接返回 true + if (status.isGranted) { + return true; + } + + // 如果被永久拒绝,提示用户前往设置页面 + if (status.isPermanentlyDenied) { + _showPermissionDialog(true); + return false; + } + + // 如果之前拒绝但不是永久拒绝,或首次请求,尝试请求权限 + if (status.isDenied) { + status = await Permission.microphone.request(); + if (status.isGranted) { + return true; + } else { + _showPermissionDialog(false); + return false; + } + } + + return false; + } catch (e) { + logger.e('检查麦克风权限失败: $e'); + return false; + } + } + + // 显示权限提示对话框 + void _showPermissionDialog(bool isPermanent) { + Get.dialog( + AlertDialog( + title: const Text('需要麦克风权限'), + content: Text(isPermanent + ? '语音功能需要麦克风权限。请前往设置页面手动开启权限。' + : '语音功能需要麦克风权限。请允许使用麦克风权限以继续使用语音功能。'), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('取消'), + ), + TextButton( + onPressed: () { + Get.back(); + if (isPermanent) { + AppSettings.openAppSettings(); + } else { + Permission.microphone.request(); + } + }, + child: Text(isPermanent ? '前往设置' : '授权'), + ), + ], + ), + ); + } + // 开始语音输入 Future startVoiceInput() async { if (isListening.value) return; + // 检查麦克风权限 + final hasPermission = await _checkMicrophonePermission(); + if (!hasPermission) { + isTextInputMode.value = true; // 如果没有权限,切回文本模式 + return; + } + try { await AgentService.startConversation(); } catch (e) { @@ -521,26 +655,6 @@ class AgentController extends GetxController { } } - // 切换输入模式 - void toggleInputMode() { - // 如果当前在图片输入模式,先清除 - if (isImageInputActive.value) { - clearImageInput(); - return; - } - - isTextInputMode.toggle(); - - // 切换到语音模式时,直接开始语音输入 - if (!isTextInputMode.value) { - startVoiceInput(); - } - // 切换到文本模式时,直接停止语音输入 - else { - stopVoiceInput(); - } - } - // 从相册选择图片 Future pickImage() async { try { @@ -645,4 +759,13 @@ class AgentController extends GetxController { _musiceManager.resume(); isPlaying.value = true; } + + // 修改自动停止处理,遵循TTS设置 + void _handleAutoStop(int index) { + // 发送此消息到AI处理 + isProcessing.value = true; + _isNewAssistantResponse = true; + AgentService.processTextInput(messages[index].text, + speakResponse: isTtsEnabled.value); + } } diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index be56a48f9..592df110e 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -10,42 +10,64 @@ class AgentView extends StatelessWidget { @override Widget build(BuildContext context) { final controller = Get.find(); - + final theme = Theme.of(context); + final primaryColor = theme.primaryColor; + final backgroundColor = theme.scaffoldBackgroundColor; + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + return Scaffold( appBar: AppBar( + elevation: 1, + backgroundColor: backgroundColor, + leading: IconButton( + icon: Icon(Icons.arrow_back_ios, + color: isDarkMode ? Colors.white : Colors.black87, + size: 20), + onPressed: () => Get.back(), + ), + titleSpacing: 0, title: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( - color: Theme.of(context).primaryColor, + color: primaryColor, borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 3, + offset: const Offset(0, 1), + ), + ], ), child: const Icon( Icons.smart_toy_outlined, color: Colors.white, + size: 22, ), ), - const SizedBox(width: 10), + const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( 'DeepVoice AI', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, + color: isDarkMode ? Colors.white : Colors.black87, ), ), Row( children: [ Container( - width: 8, - height: 8, - decoration: BoxDecoration( + width: 6, + height: 6, + decoration: const BoxDecoration( color: Colors.green, - borderRadius: BorderRadius.circular(4), + shape: BoxShape.circle, ), ), const SizedBox(width: 4), @@ -54,6 +76,7 @@ class AgentView extends StatelessWidget { style: TextStyle( fontSize: 12, color: Colors.green, + fontWeight: FontWeight.w500, ), ), ], @@ -63,14 +86,62 @@ class AgentView extends StatelessWidget { ], ), actions: [ - PopupMenuButton( - icon: const Icon(Icons.more_vert), - itemBuilder: (context) => [ - PopupMenuItem( - child: const Text('清除聊天记录'), - onTap: () => controller.clearChat(), + // TTS开关 + Obx(() => IconButton( + icon: Icon( + controller.isTtsEnabled.value + ? Icons.volume_up + : Icons.volume_off, + color: controller.isTtsEnabled.value + ? primaryColor + : isDarkMode + ? Colors.grey[400] + : Colors.grey, + ), + onPressed: () => controller.toggleTts(), + )), + + // 更多选项 + Container( + width: 34, + height: 34, + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], + shape: BoxShape.circle, + boxShadow: [ + if (!isDarkMode) + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 2, + spreadRadius: 0.5, + ), + ], + ), + child: PopupMenuButton( + icon: Icon(Icons.more_vert, + color: isDarkMode ? Colors.white : Colors.black87, + size: 20), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), ), - ], + color: isDarkMode ? Colors.grey[800] : Colors.white, + itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + Icon(Icons.delete_outline, color: primaryColor, size: 20), + const SizedBox(width: 8), + const Text('清除聊天记录', style: TextStyle(fontSize: 14)), + ], + ), + onTap: () => Future.delayed( + const Duration(milliseconds: 100), + () => controller.clearChat(), + ), + ), + ], + ), ), ], ), @@ -81,19 +152,22 @@ class AgentView extends StatelessWidget { child: Obx( () => ListView.builder( controller: controller.scrollController, - reverse: false, - padding: const EdgeInsets.all(16), + reverse: true, + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), itemCount: controller.messages.length, itemBuilder: (context, index) { - final message = controller.messages[index]; - return MessageBubble( - isUser: message.isUser, - message: message.text, - timestamp: message.timestamp, - isRecognizing: message.isRecognizing, - hasImage: message.hasImage, - imagePath: message.imagePath, - mate: message.meta, + final message = controller.messages[controller.messages.length - 1 - index]; + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: MessageBubble( + isUser: message.isUser, + message: message.text, + timestamp: message.timestamp, + isRecognizing: message.isRecognizing, + hasImage: message.hasImage, + imagePath: message.imagePath, + mate: message.meta, + ), ); }, ), @@ -102,16 +176,22 @@ class AgentView extends StatelessWidget { // 输入区域 Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, + color: backgroundColor, boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 5, + color: Colors.black.withOpacity(0.06), + blurRadius: 8, offset: const Offset(0, -1), ), ], + border: Border( + top: BorderSide( + color: Colors.grey.withOpacity(0.15), + width: 1, + ), + ), ), child: Column( mainAxisSize: MainAxisSize.min, @@ -141,12 +221,12 @@ class AgentView extends StatelessWidget { child: GestureDetector( onTap: () => controller.clearImageInput(), child: Container( - padding: EdgeInsets.all(4), + padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.black.withOpacity(0.6), shape: BoxShape.circle, ), - child: Icon( + child: const Icon( Icons.close, color: Colors.white, size: 16, @@ -158,7 +238,7 @@ class AgentView extends StatelessWidget { ), // 预设提示选项 - SizedBox(height: 8), + const SizedBox(height: 12), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( @@ -168,13 +248,13 @@ class AgentView extends StatelessWidget { label: '总结图片内容', onTap: () => controller.summarizeImage(), ), - SizedBox(width: 8), + const SizedBox(width: 8), _buildPromptChip( context: context, label: '抽取图像文字', onTap: () => controller.extractTextFromImage(), ), - SizedBox(width: 8), + const SizedBox(width: 8), _buildPromptChip( context: context, label: '翻译图像文字', @@ -183,11 +263,11 @@ class AgentView extends StatelessWidget { ], ), ), - SizedBox(height: 12), + const SizedBox(height: 12), ], ); } else { - return SizedBox.shrink(); + return const SizedBox.shrink(); } }), @@ -202,15 +282,16 @@ class AgentView extends StatelessWidget { controller.toggleInputMode(); }, child: Container( - width: 48, - height: 48, + width: 44, + height: 44, decoration: BoxDecoration( color: Colors.grey[200], - borderRadius: BorderRadius.circular(24), + borderRadius: BorderRadius.circular(22), ), child: Icon( isTextMode ? Icons.mic_none : Icons.keyboard, - color: Colors.grey[600], + color: Colors.grey[700], + size: 22, ), ), ); @@ -229,7 +310,11 @@ class AgentView extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: Colors.grey[200], - borderRadius: BorderRadius.circular(24), + borderRadius: BorderRadius.circular(22), + border: Border.all( + color: Colors.grey.withOpacity(0.1), + width: 1.0, + ), ), child: Row( children: [ @@ -243,10 +328,18 @@ class AgentView extends StatelessWidget { controller: controller.textController, maxLines: isImageActive ? 2 : 1, minLines: isImageActive ? 2 : 1, + style: TextStyle( + fontSize: 15, + color: isDarkMode ? Colors.white : Colors.black87, + ), decoration: InputDecoration( hintText: isImageActive ? '添加对图片的问题或描述...' : '输入消息...', + hintStyle: TextStyle( + color: Colors.grey[500], + fontSize: 15, + ), border: InputBorder.none, contentPadding: EdgeInsets.symmetric( vertical: isImageActive ? 12 : 0), @@ -258,9 +351,13 @@ class AgentView extends StatelessWidget { ), GestureDetector( onTap: () => controller.sendTextMessage(), - child: Icon( - Icons.send, - color: Theme.of(context).primaryColor, + child: Container( + padding: const EdgeInsets.all(6), + child: Icon( + Icons.send, + color: primaryColor, + size: 22, + ), ), ), ], @@ -277,12 +374,16 @@ class AgentView extends StatelessWidget { } }, child: Container( - height: 48, + height: 44, padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: Colors.grey[200], - borderRadius: BorderRadius.circular(24), + borderRadius: BorderRadius.circular(22), + border: Border.all( + color: Colors.grey.withOpacity(0.1), + width: 1.0, + ), ), child: Row( children: [ @@ -306,19 +407,18 @@ class AgentView extends StatelessWidget { Icons.mic, size: 14, color: isListening - ? Theme.of(context) - .primaryColor - : Colors.grey, + ? primaryColor + : Colors.grey[500], ), const SizedBox(width: 4), Text( '正在聆听', style: TextStyle( - fontSize: 12, + fontSize: 13, + fontWeight: FontWeight.w500, color: isListening - ? Theme.of(context) - .primaryColor - : Colors.grey, + ? primaryColor + : Colors.grey[500], ), ), ], @@ -339,18 +439,22 @@ class AgentView extends StatelessWidget { // 图片选择按钮 Container( - width: 48, - height: 48, + width: 44, + height: 44, decoration: BoxDecoration( color: Colors.grey[200], - borderRadius: BorderRadius.circular(24), + borderRadius: BorderRadius.circular(22), ), child: PopupMenuButton( icon: Icon( Icons.add_photo_alternate_outlined, - color: Colors.grey[600], + color: Colors.grey[700], + size: 22, ), padding: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), onSelected: (value) async { if (value == 'camera') { await controller.takePhoto(); @@ -364,9 +468,9 @@ class AgentView extends StatelessWidget { child: Row( children: [ Icon(Icons.camera_alt, - color: Theme.of(context).primaryColor), - SizedBox(width: 8), - Text('拍照'), + color: primaryColor, size: 20), + const SizedBox(width: 8), + const Text('拍照', style: TextStyle(fontSize: 14)), ], ), ), @@ -375,9 +479,9 @@ class AgentView extends StatelessWidget { child: Row( children: [ Icon(Icons.photo_library, - color: Theme.of(context).primaryColor), - SizedBox(width: 8), - Text('从相册选择'), + color: primaryColor, size: 20), + const SizedBox(width: 8), + const Text('从相册选择', style: TextStyle(fontSize: 14)), ], ), ), @@ -400,22 +504,26 @@ class AgentView extends StatelessWidget { required String label, required VoidCallback onTap, }) { + final theme = Theme.of(context); + final primaryColor = theme.primaryColor; + return GestureDetector( onTap: onTap, child: Container( - padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withOpacity(0.1), + color: primaryColor.withOpacity(0.08), borderRadius: BorderRadius.circular(16), border: Border.all( - color: Theme.of(context).primaryColor.withOpacity(0.3), + color: primaryColor.withOpacity(0.2), + width: 1, ), ), child: Text( label, style: TextStyle( fontSize: 13, - color: Theme.of(context).primaryColor, + color: primaryColor, fontWeight: FontWeight.w500, ), ), @@ -463,7 +571,7 @@ class _SoundBarState extends State<_SoundBar> duration: const Duration(milliseconds: 500), ); - _animation = Tween(begin: 3, end: 15).animate( + _animation = Tween(begin: 3, end: 18).animate( CurvedAnimation( parent: _controller, curve: Curves.easeInOut, @@ -493,7 +601,7 @@ class _SoundBarState extends State<_SoundBar> } else { _controller.reset(); // 设置为小高度的静态状态 - _controller.animateTo(0.1, duration: Duration(milliseconds: 300)); + _controller.animateTo(0.1, duration: const Duration(milliseconds: 300)); } } @@ -505,13 +613,17 @@ class _SoundBarState extends State<_SoundBar> @override Widget build(BuildContext context) { + final primaryColor = Theme.of(context).primaryColor; + return Container( - width: 2, + width: 2.5, height: widget.isActive ? _animation.value : 3, + margin: const EdgeInsets.symmetric(horizontal: 1), decoration: BoxDecoration( - color: - widget.isActive ? Theme.of(context).primaryColor : Colors.grey[400], - borderRadius: BorderRadius.circular(1), + color: widget.isActive + ? primaryColor.withOpacity(0.8) + : Colors.grey.withOpacity(0.4), + borderRadius: BorderRadius.circular(1.5), ), ); } diff --git a/lib/modules/agent/views/message_bubble.dart b/lib/modules/agent/views/message_bubble.dart index 952551e18..eb0f9f201 100644 --- a/lib/modules/agent/views/message_bubble.dart +++ b/lib/modules/agent/views/message_bubble.dart @@ -197,7 +197,7 @@ class MessageBubble extends GetView { // 判断消息内容,显示特殊卡片 if (!isUser && mate != null) { if (mate!.keys.contains('card_music')) { - final musicId = mate!['card_music']['id'] as String; + final musicId = mate!['card_music']['id'].toString(); final musicName = mate!['card_music']['name'] as String; final musicUrl = mate!['card_music']['url'] as String; final musicSinger = mate!['card_music']['sgener'] as String; @@ -326,7 +326,7 @@ class MessageBubble extends GetView { ...playlist.asMap().entries.map((entry) { final index = entry.key; final item = entry.value; - final id = item["id"] ?? ""; + final id = item["id"]?.toString() ?? ""; final name = item["name"] ?? "未知歌曲"; final url = item["url"] ?? ""; final singer = item["sgener"] ?? "未知歌手"; diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 7ecf4e076..98d7df449 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -186,7 +186,7 @@ class PairingController extends GetxController { if (Platform.isAndroid) { permissionsToRequest.add(Permission.locationWhenInUse); - permissionsToRequest.add(Permission.microphone); + // permissionsToRequest.add(Permission.microphone); if (await _getAndroidSdkVersion() >= 31) { permissionsToRequest.add(Permission.bluetoothScan); permissionsToRequest.add(Permission.bluetoothConnect); 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 7ac469322..56fbded96 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 @@ -666,12 +666,11 @@ object AgentService : CoroutineScope { override fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) { audioPlayer?.stopAudio() - FileLogger.d(TAG, "mcp调用结果: $functionCallResult") sendEvent("function_call_result", mapOf( "function_call" to functionCall.toString(), "result" to functionCallResult.toString(), )) - FileLogger.d(TAG, "mcp调用结果: $functionCallResult") + FileLogger.d(TAG, "mcp调用结果: $functionCallResult") metadata = autoPlayMusic(functionCallResult); } } diff --git a/local_plugins/music_service/android/src/main/AndroidManifest.xml b/local_plugins/music_service/android/src/main/AndroidManifest.xml index 3469dab1e..4cab7c479 100644 --- a/local_plugins/music_service/android/src/main/AndroidManifest.xml +++ b/local_plugins/music_service/android/src/main/AndroidManifest.xml @@ -7,7 +7,9 @@ + android:exported="true" + android:foregroundServiceType="mediaPlayback" + > @@ -20,7 +22,7 @@ + android:exported="true"> diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt index e0091e1ff..1274555d4 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt @@ -55,6 +55,7 @@ class MusicService : MediaBrowserServiceCompat() { } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + FileLogger.i(TAG, "onStartCommand: $intent") intent?.action?.takeIf { it == Intent.ACTION_MEDIA_BUTTON }?.let { // 分发给 MediaSession 处理媒体按钮 MediaButtonReceiver.handleIntent(mediaSession, intent) @@ -62,7 +63,6 @@ class MusicService : MediaBrowserServiceCompat() { } val command = intent?.getStringExtra("command") ?: return START_NOT_STICKY - FileLogger.i(TAG, "liwei------------------- onStartCommand: $command") when (command) { "play" -> intent.getSerializableExtra("song")?.let { url -> val songMap = intent.getSerializableExtra("song") as? Map diff --git a/local_plugins/open_ai_service/android/build.gradle.kts b/local_plugins/open_ai_service/android/build.gradle.kts index d1247752b..f5bbc9337 100644 --- a/local_plugins/open_ai_service/android/build.gradle.kts +++ b/local_plugins/open_ai_service/android/build.gradle.kts @@ -60,4 +60,5 @@ dependencies { implementation("io.ktor:ktor-client-logging:3.1.2") implementation("com.google.code.gson:gson:2.10.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") + implementation("androidx.media:media:1.6.0") } \ No newline at end of file diff --git a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt index 0349b06be..8b70b05e2 100644 --- a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt +++ b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt @@ -528,7 +528,7 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope { val call = client.newCall(request) // 保存当前Call对象以便后续取消 currentStreamCall = call - Log.d(TAG, "发送消息流式输出: request=$request") + // Log.d(TAG, "发送消息流式输出: request=$request") call.enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { Log.d(TAG, "发送消息流式输出失败: e=$e") @@ -638,7 +638,7 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope { // 处理普通文本内容 if (delta.has("content")) { val content = delta.getString("content") - Log.d(TAG, "处理流式JSON数据: content=$content") + // Log.d(TAG, "处理流式JSON数据: content=$content") if (!isCanceled) { callback.onToken(content) } diff --git a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt index bf5b98ddc..e197f4892 100644 --- a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt +++ b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt @@ -17,7 +17,7 @@ import android.util.Log import org.json.JSONObject import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume - +import android.view.KeyEvent /** * 系统功能处理器 - 负责调用系统API执行各种功能以及注册系统功能 */ @@ -430,27 +430,22 @@ class SystemFunctionHandler(private val context: Context) { ) flags = Intent.FLAG_RECEIVER_FOREGROUND } - context.sendOrderedBroadcast(keyIntent, null) + keyIntent.setPackage("com.yunqiinnovation.deep_voice") + context.sendBroadcast(keyIntent) return Result( success = true, message = "已开始播放媒体" ) } "pause" -> { - // 尝试发送暂停广播 - intent.action = "android.intent.action.MEDIA_PAUSE" - context.sendBroadcast(intent) + // 发送完整的按键事件序列 + val downEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PAUSE) + val keyIntent = Intent(Intent.ACTION_MEDIA_BUTTON) + keyIntent.putExtra(Intent.EXTRA_KEY_EVENT, downEvent) + keyIntent.setPackage("com.yunqiinnovation.deep_voice") + context.sendBroadcast(keyIntent) - // 也尝试使用媒体按键广播 - val keyIntent = Intent(Intent.ACTION_MEDIA_BUTTON).apply { - putExtra( - Intent.EXTRA_KEY_EVENT, - android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_PAUSE) - ) - flags = Intent.FLAG_RECEIVER_FOREGROUND - } - context.sendOrderedBroadcast(keyIntent, null) - return Result( + return Result( success = true, message = "已暂停媒体播放" ) @@ -468,7 +463,8 @@ class SystemFunctionHandler(private val context: Context) { ) flags = Intent.FLAG_RECEIVER_FOREGROUND } - context.sendOrderedBroadcast(keyIntent, null) + keyIntent.setPackage("com.yunqiinnovation.deep_voice") + context.sendBroadcast(keyIntent) return Result( success = true, message = "已切换到上一首" @@ -487,7 +483,8 @@ class SystemFunctionHandler(private val context: Context) { ) flags = Intent.FLAG_RECEIVER_FOREGROUND } - context.sendOrderedBroadcast(keyIntent, null) + keyIntent.setPackage("com.yunqiinnovation.deep_voice") + context.sendBroadcast(keyIntent) return Result( success = true, message = "已切换到下一首" From 4795d6f2512f1297427cd818121c32641387932e Mon Sep 17 00:00:00 2001 From: wolfplus Date: Tue, 13 May 2025 11:15:47 +0100 Subject: [PATCH 4/7] add --- .../music_service/android/src/main/AndroidManifest.xml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/local_plugins/music_service/android/src/main/AndroidManifest.xml b/local_plugins/music_service/android/src/main/AndroidManifest.xml index 53ef4ae30..0b1f1fde9 100644 --- a/local_plugins/music_service/android/src/main/AndroidManifest.xml +++ b/local_plugins/music_service/android/src/main/AndroidManifest.xml @@ -7,14 +7,13 @@ + android:exported="true" + android:foregroundServiceType="mediaPlayback" + > - - - - + From 53d3a32f485621408c1da23e3f9f02b0e643ffca Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Tue, 13 May 2025 18:42:25 +0800 Subject: [PATCH 5/7] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agent/controllers/agent_controller.dart | 47 +++++++++---------- .../open_ai_service/OpenAIService.kt | 2 +- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index d1b41afd0..617cd5258 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -116,12 +116,12 @@ class AgentController extends GetxController { // 切换TTS状态 Future toggleTts() async { isTtsEnabled.toggle(); - + // 如果关闭TTS,停止当前语音 if (!isTtsEnabled.value && isSpeaking.value) { await AgentService.stopTts(); } - + // 保存偏好设置 try { final storage = GetStorage(); @@ -382,22 +382,15 @@ class AgentController extends GetxController { logger.i('Flutter 结束调用 mcp: $result, $functionCall'); if (result.isNotEmpty) { final parsedResult = jsonDecode(result); - final meta = parsedResult["meta"]; - Map metaResult = {}; - - if (meta != null && meta is String && meta.isNotEmpty) { - try { - metaResult = jsonDecode(meta); - } catch (e) { - print('解析meta数据失败: $e'); - } - } - if(metaResult.isNotEmpty){ - + final meta = parsedResult["meta"] as String; + final metaResult = jsonDecode(meta); + if (meta.isNotEmpty) { + logger.i('Flutter 结束调用 mcp metaResult: $metaResult'); // 判断是否为新的回复或响应ID是否改变 if (_isNewAssistantResponse || _currentResponseId != responseId) { // 添加新消息 - messages.add(Message(isUser: false, text: "", meta: metaResult)); + messages + .add(Message(isUser: false, text: "", meta: metaResult)); _isNewAssistantResponse = false; _currentResponseId = responseId; } else { @@ -407,11 +400,14 @@ class AgentController extends GetxController { messages[lastIndex] = Message( isUser: false, text: messages[lastIndex].text, - meta: meta, + meta: metaResult, timestamp: messages[lastIndex].timestamp, ); } } + messages.refresh(); + // 滚动到底部 + _scrollToBottom(); } } break; @@ -467,7 +463,8 @@ class AgentController extends GetxController { // 标记为新的AI回复 _isNewAssistantResponse = true; // 根据TTS状态决定是否朗读 - await AgentService.processTextInput(text, speakResponse: isTtsEnabled.value); + await AgentService.processTextInput(text, + speakResponse: isTtsEnabled.value); } catch (e) { logger.e('发送文本消息失败: $e'); isProcessing.value = false; @@ -549,18 +546,18 @@ class AgentController extends GetxController { Future _checkMicrophonePermission() async { try { PermissionStatus status = await Permission.microphone.status; - + // 如果已授权,直接返回 true if (status.isGranted) { return true; } - + // 如果被永久拒绝,提示用户前往设置页面 if (status.isPermanentlyDenied) { _showPermissionDialog(true); return false; } - + // 如果之前拒绝但不是永久拒绝,或首次请求,尝试请求权限 if (status.isDenied) { status = await Permission.microphone.request(); @@ -571,22 +568,22 @@ class AgentController extends GetxController { return false; } } - + return false; } catch (e) { logger.e('检查麦克风权限失败: $e'); return false; } } - + // 显示权限提示对话框 void _showPermissionDialog(bool isPermanent) { Get.dialog( AlertDialog( title: const Text('需要麦克风权限'), - content: Text(isPermanent - ? '语音功能需要麦克风权限。请前往设置页面手动开启权限。' - : '语音功能需要麦克风权限。请允许使用麦克风权限以继续使用语音功能。'), + content: Text(isPermanent + ? '语音功能需要麦克风权限。请前往设置页面手动开启权限。' + : '语音功能需要麦克风权限。请允许使用麦克风权限以继续使用语音功能。'), actions: [ TextButton( onPressed: () => Get.back(), diff --git a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt index 8b70b05e2..6dd0bd660 100644 --- a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt +++ b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt @@ -698,7 +698,7 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope { put("arguments", firstToolCall.arguments) put("id", firstToolCall.id) } - Log.d(TAG, "工具调用: id=${firstToolCall.id}, name=${firstToolCall.name}") + Log.d(TAG, "工具调用: id=${firstToolCall.id}, name=${firstToolCall.name} arguments=${firstToolCall.arguments}") // 通知上层工具调用事件 callback.onFunctionCall(functionCall) From 8353f47e3402f52654ae09dc90858b1a4af31162 Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Tue, 13 May 2025 18:52:23 +0800 Subject: [PATCH 6/7] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BC=9A=E8=AE=AE?= =?UTF-8?q?=E8=BD=AC=E5=86=99=E8=AF=86=E5=88=AB=E4=B8=8D=E5=87=BA=E6=9D=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../meeting/controllers/meeting_details_controller.dart | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/modules/meeting/controllers/meeting_details_controller.dart b/lib/modules/meeting/controllers/meeting_details_controller.dart index e602c00eb..b906902ed 100644 --- a/lib/modules/meeting/controllers/meeting_details_controller.dart +++ b/lib/modules/meeting/controllers/meeting_details_controller.dart @@ -110,10 +110,10 @@ class MeetingDetailsController extends GetxController void _getAgent() async { final GetStorage storage = GetStorage(); - final List agents = storage.read("AGENTS"); + // final List agents = storage.read("AGENTS"); Map userInfo = storage.read("user_info"); _userId = userInfo['user']['uid']; - _agent = agents.firstWhere((agent) => agent.id == 'meet_assistant'); + // _agent = agents.firstWhere((agent) => agent.id == 'meet_assistant'); } void _getDataDetails() async { diff --git a/pubspec.yaml b/pubspec.yaml index 37e68bb5b..608ba936e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,7 +34,7 @@ dependencies: webview_flutter: ^4.10.0 flutter_easyloading: ^3.0.5 package_info_plus: ^8.3.0 - file_picker: ^8.0.0+1 + file_picker: ^10.1.5 haptic_feedback: ^0.5.1+1 tencentcloud_cos_sdk_plugin: ^1.2.3 From 58752aab2b278e933a654526e4cfbb16772e3c02 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Tue, 13 May 2025 19:41:59 +0800 Subject: [PATCH 7/7] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/modules/pairing/controllers/pairing_controller.dart | 3 +++ .../com/yunqiinnovation/azure_speech/AzureAsrHelper.kt | 5 ++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 98d7df449..7a65da691 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -235,6 +235,9 @@ class PairingController extends GetxController { } _updateStatusDescription(); }); + if (needdeivice) { + _handleDeviceConnected(); + } } /// 处理设备连接成功事件 diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index a30bfdb36..54a68eb56 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -606,9 +606,8 @@ class AzureAsrHelper(private val context: Context) { val sessionId = audioRecord?.audioSessionId ?: return // 回音消除 - echoCanceler = AcousticEchoCanceler.create(sessionId).apply { - enabled = true - } + echoCanceler = AcousticEchoCanceler.create(sessionId) + echoCanceler?.enabled = true; } /**