From b30376a5e37777e296b9ab788ff4fdfcbd4f766b Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Tue, 13 May 2025 17:29:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E4=BC=9A=E8=AE=AE=E6=A8=A1=E5=9D=97):=20t?= =?UTF-8?q?anls=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: