Browse Source

Merge commit '58752aab2b278e933a654526e4cfbb16772e3c02' into lxm

weicu
lxm 1 year ago
parent
commit
86d579a8ca
  1. 88
      lib/core/utils/upload_oss.dart
  2. 26
      lib/data/services/db/database_helper.dart
  3. 109
      lib/data/services/db/sqflite_api.dart
  4. 244
      lib/modules/agent/controllers/agent_controller.dart
  5. 260
      lib/modules/agent/views/agent_view.dart
  6. 4
      lib/modules/agent/views/message_bubble.dart
  7. 11
      lib/modules/meeting/bindings/meeting_edit_binding.dart
  8. 10
      lib/modules/meeting/bindings/meeting_record_binding.dart
  9. 79
      lib/modules/meeting/controllers/meeting_controller.dart
  10. 361
      lib/modules/meeting/controllers/meeting_details_controller.dart
  11. 143
      lib/modules/meeting/controllers/meeting_edit_controller.dart
  12. 324
      lib/modules/meeting/controllers/meeting_record_controller.dart
  13. 30
      lib/modules/meeting/model/meeting_details_model.dart
  14. 21
      lib/modules/meeting/model/meeting_model.dart
  15. 566
      lib/modules/meeting/views/meeting_details_view.dart
  16. 301
      lib/modules/meeting/views/meeting_edit_view.dart
  17. 363
      lib/modules/meeting/views/meeting_record_view.dart
  18. 322
      lib/modules/meeting/views/meeting_view.dart
  19. 12
      lib/modules/meeting/views/mind_map_tab.dart
  20. 12
      lib/modules/meeting/views/overview_tab.dart
  21. 202
      lib/modules/meeting/views/speaker_bottom_sheet.dart
  22. 256
      lib/modules/meeting/views/speech_tab.dart
  23. 226
      lib/modules/meeting/views/summary_tab.dart
  24. 5
      lib/modules/pairing/controllers/pairing_controller.dart
  25. 18
      lib/routes/app_pages.dart
  26. 4
      lib/routes/app_routes.dart
  27. 3
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  28. 5
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  29. 11
      local_plugins/music_service/android/src/main/AndroidManifest.xml
  30. 2
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt
  31. 1
      local_plugins/open_ai_service/android/build.gradle.kts
  32. 2
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt
  33. 6
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt
  34. 31
      local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt
  35. 2
      pubspec.yaml

88
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<CosTransferManger> 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<String> 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<String>();
await cosTransferManger.upload(
bucket,
pathName,
filePath: filepath,
resultListener: ResultListener(
(Map<String?, String?>? 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<String> array = path.split('.');
return array[array.length - 1];
}
}

26
lib/data/services/db/database_helper.dart

@ -30,11 +30,10 @@ class DatabaseHelper {
CREATE TABLE IF NOT EXISTS meeting ( CREATE TABLE IF NOT EXISTS meeting (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
title TEXT, title TEXT,
content TEXT, type TEXT,
seconds INTEGER, seconds INTEGER,
address TEXT, filepath TEXT,
longitude TEXT, audiourl TEXT,
latitude TEXT,
creationtime INTEGER creationtime INTEGER
) )
'''); ''');
@ -43,9 +42,24 @@ class DatabaseHelper {
CREATE TABLE IF NOT EXISTS meetingdetails ( CREATE TABLE IF NOT EXISTS meetingdetails (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
meetingid INTEGER, 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, content TEXT,
aisummary TEXT starttime INTEGER,
endtime INTEGER,
speaker TEXT
) )
'''); ''');
await batch.commit(); await batch.commit();

109
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<String, Object?> meeting) async { static Future insertMeeting(Map<String, Object?> meeting) async {
final db = await DatabaseHelper().database; final db = await DatabaseHelper().database;
try { try {
int id = 0; int id = 0;
await db.transaction((txn) async { await db.transaction((txn) async {
id = await txn.insert('meeting', { id = await txn.insert('meeting', meeting);
'title': meeting['title'],
'content': meeting['firstContent'],
'seconds': meeting['seconds'],
'address': meeting['address'],
'longitude': meeting['longitude'],
'latitude': meeting['latitude'],
'creationtime': meeting['creationTime'],
});
await txn.insert('meetingdetails', { await txn.insert('meetingdetails', {
'meetingid': id, 'meetingid': id,
'audio': meeting['audio'], 'address': '',
'content': meeting['content'], 'personnel': '',
'aisummary': meeting['aisummary'], 'taskid': '',
'tasktype': 0,
'overview': '',
'summary': '',
'mindmap': '',
}); });
}); });
return id; return id;
@ -66,34 +72,65 @@ class SqfliteApi {
} }
} }
// 修改商务会议助手 // 修改商务会议助手详情内容
static Future editMeeting(id, Map<String, Object?> meeting) async { static Future editMeeting(id, Map<String, Object?> meeting) async {
final db = await DatabaseHelper().database; final db = await DatabaseHelper().database;
try { try {
await db.transaction((txn) async { await db.update(
await txn.update( 'meetingdetails',
'meeting', meeting,
{ where: 'meetingid = ?',
'title': meeting['title'], whereArgs: [id],
'content': meeting['firstContent'], );
'seconds': meeting['seconds'], return true;
'address': meeting['address'], } catch (e) {
'longitude': meeting['longitude'], Get.snackbar(
'latitude': meeting['latitude'], 'error'.tr, // 错误
}, 'unknownError'.tr,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
return;
}
}
// 插入商务会议助手转写内容
static Future insertMeetingSpeaker(List<Map<String, Object?>> 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 = ?', where: 'id = ?',
whereArgs: [id], whereArgs: [item['id']],
);
await txn.update(
'meetingdetails',
{
'content': meeting['content'],
'aisummary': meeting['aisummary'],
},
where: 'meetingid = ?',
whereArgs: [id],
); );
}); }
await batch.commit();
return true; return true;
} catch (e) { } catch (e) {
Get.snackbar( Get.snackbar(
@ -121,6 +158,8 @@ class SqfliteApi {
await txn.delete('meeting', where: 'id = ?', whereArgs: [id]); await txn.delete('meeting', where: 'id = ?', whereArgs: [id]);
await txn await txn
.delete('meetingdetails', where: 'meetingid = ?', whereArgs: [id]); .delete('meetingdetails', where: 'meetingid = ?', whereArgs: [id]);
await txn
.delete('meetingspeaker', where: 'meetingid = ?', whereArgs: [id]);
}); });
} }
} }

244
lib/modules/agent/controllers/agent_controller.dart

@ -8,6 +8,9 @@ import 'package:agent_service/agent_service.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:chat_storage/chat_storage.dart'; import 'package:chat_storage/chat_storage.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:get_storage/get_storage.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:app_settings/app_settings.dart';
class Message { class Message {
final bool isUser; final bool isUser;
@ -51,6 +54,8 @@ class AgentController extends GetxController {
final isListening = false.obs; final isListening = false.obs;
final isSpeaking = false.obs; final isSpeaking = false.obs;
final isProcessing = false.obs; final isProcessing = false.obs;
// TTS启用状态
final isTtsEnabled = true.obs;
// 图片处理状态 // 图片处理状态
final isImageProcessing = false.obs; final isImageProcessing = false.obs;
@ -81,6 +86,7 @@ class AgentController extends GetxController {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
_loadTtsPreference();
_loadChatHistory(); _loadChatHistory();
_subscribeToEvents(); _subscribeToEvents();
} }
@ -95,6 +101,36 @@ class AgentController extends GetxController {
super.onClose(); super.onClose();
} }
// 加载TTS偏好设置
Future<void> _loadTtsPreference() async {
try {
final storage = GetStorage();
isTtsEnabled.value = storage.read('agent_tts_enabled') ?? true;
} catch (e) {
// 如果读取失败,使用默认值
logger.e('加载TTS偏好设置失败: $e');
isTtsEnabled.value = true;
}
}
// 切换TTS状态
Future<void> toggleTts() async {
isTtsEnabled.toggle();
// 如果关闭TTS,停止当前语音
if (!isTtsEnabled.value && isSpeaking.value) {
await AgentService.stopTts();
}
// 保存偏好设置
try {
final storage = GetStorage();
await storage.write('agent_tts_enabled', isTtsEnabled.value);
} catch (e) {
logger.e('保存TTS偏好设置失败: $e');
}
}
// 加载聊天历史记录 // 加载聊天历史记录
Future<void> _loadChatHistory() async { Future<void> _loadChatHistory() async {
try { try {
@ -134,12 +170,8 @@ class AgentController extends GetxController {
_addWelcomeMessage(); _addWelcomeMessage();
} finally { } finally {
isHistoryLoaded.value = true; isHistoryLoaded.value = true;
messages.refresh(); messages.refresh();
// 在加载完成后直接定位到底部,无需动画 // 使用reverse:true时不需要手动滚动到底部
WidgetsBinding.instance.addPostFrameCallback((_) {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
});
} }
} }
@ -199,7 +231,7 @@ class AgentController extends GetxController {
)); ));
} }
messages.refresh(); messages.refresh();
_scrollToBottom(); // 使用reverse:true时通常不需要手动滚动
} }
break; break;
@ -229,8 +261,7 @@ class AgentController extends GetxController {
isProcessing.value = true; isProcessing.value = true;
// 标记为新的AI回复 // 标记为新的AI回复
_isNewAssistantResponse = true; _isNewAssistantResponse = true;
// 滚动到底部 // 使用reverse:true时通常不需要手动滚动
_scrollToBottom();
} else if (index >= 0) { } else if (index >= 0) {
// 如果识别结果为空,移除临时的识别消息 // 如果识别结果为空,移除临时的识别消息
messages.removeAt(index); messages.removeAt(index);
@ -282,8 +313,7 @@ class AgentController extends GetxController {
); );
} }
} }
// 滚动到底部 // 使用reverse:true时通常不需要手动滚动
_scrollToBottom();
} }
break; break;
@ -334,8 +364,7 @@ class AgentController extends GetxController {
// 发送此消息到AI处理 // 发送此消息到AI处理
isProcessing.value = true; isProcessing.value = true;
_isNewAssistantResponse = true; _isNewAssistantResponse = true;
AgentService.processTextInput(messages[index].text, _handleAutoStop(index);
speakResponse: true);
} }
} }
break; break;
@ -351,33 +380,35 @@ class AgentController extends GetxController {
final result = event.data['result'] ?? ''; final result = event.data['result'] ?? '';
final functionCall = event.data['function_call'] ?? ''; final functionCall = event.data['function_call'] ?? '';
logger.i('Flutter 结束调用 mcp: $result, $functionCall'); logger.i('Flutter 结束调用 mcp: $result, $functionCall');
if (result.isNotEmpty) { if (result.isNotEmpty) {
final parsedResult = jsonDecode(result); final parsedResult = jsonDecode(result);
final meta = parsedResult["meta"] ?? ""; final meta = parsedResult["meta"] as String;
final metaResult = jsonDecode(meta); final metaResult = jsonDecode(meta);
if (meta.isNotEmpty) {
logger.i('Flutter 结束调用 mcp metaResult: $metaResult'); logger.i('Flutter 结束调用 mcp metaResult: $metaResult');
// 判断是否为新的回复或响应ID是否改变 // 判断是否为新的回复或响应ID是否改变
if (_isNewAssistantResponse || _currentResponseId != responseId) { if (_isNewAssistantResponse || _currentResponseId != responseId) {
// 添加新消息 // 添加新消息
messages.add(Message(isUser: false, text: "", meta: metaResult)); messages
_isNewAssistantResponse = false; .add(Message(isUser: false, text: "", meta: metaResult));
_currentResponseId = responseId; _isNewAssistantResponse = false;
} else { _currentResponseId = responseId;
// 更新最后一条消息 } else {
final lastIndex = messages.length - 1; // 更新最后一条消息
if (lastIndex >= 0 && !messages[lastIndex].isUser) { final lastIndex = messages.length - 1;
messages[lastIndex] = Message( if (lastIndex >= 0 && !messages[lastIndex].isUser) {
isUser: false, messages[lastIndex] = Message(
text: messages[lastIndex].text, isUser: false,
meta: meta, text: messages[lastIndex].text,
timestamp: messages[lastIndex].timestamp, meta: metaResult,
); timestamp: messages[lastIndex].timestamp,
);
}
} }
messages.refresh();
// 滚动到底部
_scrollToBottom();
} }
// 滚动到底部
_scrollToBottom();
} }
break; break;
default: default:
@ -388,11 +419,13 @@ class AgentController extends GetxController {
// 滚动到底部 // 滚动到底部
void _scrollToBottom() { void _scrollToBottom() {
// 使用reverse:true时,此方法可大部分场景下不再需要
// 保留此方法但简化实现,仅用于特殊情况下的手动滚动
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (scrollController.hasClients) { if (scrollController.hasClients) {
try { try {
scrollController.animateTo( scrollController.animateTo(
scrollController.position.maxScrollExtent, 0, // 使用reverse:true时,0表示列表底部
duration: Duration(milliseconds: 300), duration: Duration(milliseconds: 300),
curve: Curves.easeOut, curve: Curves.easeOut,
); );
@ -423,14 +456,15 @@ class AgentController extends GetxController {
messages.add(message); messages.add(message);
textController.clear(); textController.clear();
// 滚动到底部 // 使用reverse:true时通常不需要手动滚动
_scrollToBottom();
try { try {
isProcessing.value = true; isProcessing.value = true;
// 标记为新的AI回复 // 标记为新的AI回复
_isNewAssistantResponse = true; _isNewAssistantResponse = true;
await AgentService.processTextInput(text, speakResponse: true); // 根据TTS状态决定是否朗读
await AgentService.processTextInput(text,
speakResponse: isTtsEnabled.value);
} catch (e) { } catch (e) {
logger.e('发送文本消息失败: $e'); logger.e('发送文本消息失败: $e');
isProcessing.value = false; isProcessing.value = false;
@ -459,8 +493,7 @@ class AgentController extends GetxController {
// 清空文本输入 // 清空文本输入
textController.clear(); textController.clear();
// 滚动到底部 // 使用reverse:true时通常不需要手动滚动
_scrollToBottom();
try { try {
isProcessing.value = true; isProcessing.value = true;
@ -468,11 +501,11 @@ class AgentController extends GetxController {
// 标记为新的AI回复 // 标记为新的AI回复
_isNewAssistantResponse = true; _isNewAssistantResponse = true;
// 调用Agent Service处理图片 // 调用Agent Service处理图片,考虑TTS状态
await AgentService.processImageInput( await AgentService.processImageInput(
imagePath, imagePath,
text: text ?? '', text: text ?? '',
speakResponse: true, speakResponse: isTtsEnabled.value,
); );
} catch (e) { } catch (e) {
logger.e('处理图片失败: $e'); logger.e('处理图片失败: $e');
@ -481,10 +514,108 @@ class AgentController extends GetxController {
} }
} }
// 切换输入模式
void toggleInputMode() async {
// 如果当前在图片输入模式,先清除
if (isImageInputActive.value) {
clearImageInput();
return;
}
// 如果要切换到语音模式,需要先检查麦克风权限
if (isTextInputMode.value) {
final hasPermission = await _checkMicrophonePermission();
if (!hasPermission) {
return; // 权限未获取,保持文本模式
}
}
isTextInputMode.toggle();
// 切换到语音模式时,直接开始语音输入
if (!isTextInputMode.value) {
startVoiceInput();
}
// 切换到文本模式时,直接停止语音输入
else {
stopVoiceInput();
}
}
// 检查麦克风权限
Future<bool> _checkMicrophonePermission() async {
try {
PermissionStatus status = await Permission.microphone.status;
// 如果已授权,直接返回 true
if (status.isGranted) {
return true;
}
// 如果被永久拒绝,提示用户前往设置页面
if (status.isPermanentlyDenied) {
_showPermissionDialog(true);
return false;
}
// 如果之前拒绝但不是永久拒绝,或首次请求,尝试请求权限
if (status.isDenied) {
status = await Permission.microphone.request();
if (status.isGranted) {
return true;
} else {
_showPermissionDialog(false);
return false;
}
}
return false;
} catch (e) {
logger.e('检查麦克风权限失败: $e');
return false;
}
}
// 显示权限提示对话框
void _showPermissionDialog(bool isPermanent) {
Get.dialog(
AlertDialog(
title: const Text('需要麦克风权限'),
content: Text(isPermanent
? '语音功能需要麦克风权限。请前往设置页面手动开启权限。'
: '语音功能需要麦克风权限。请允许使用麦克风权限以继续使用语音功能。'),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('取消'),
),
TextButton(
onPressed: () {
Get.back();
if (isPermanent) {
AppSettings.openAppSettings();
} else {
Permission.microphone.request();
}
},
child: Text(isPermanent ? '前往设置' : '授权'),
),
],
),
);
}
// 开始语音输入 // 开始语音输入
Future<void> startVoiceInput() async { Future<void> startVoiceInput() async {
if (isListening.value) return; if (isListening.value) return;
// 检查麦克风权限
final hasPermission = await _checkMicrophonePermission();
if (!hasPermission) {
isTextInputMode.value = true; // 如果没有权限,切回文本模式
return;
}
try { try {
await AgentService.startConversation(); await AgentService.startConversation();
} catch (e) { } catch (e) {
@ -521,26 +652,6 @@ class AgentController extends GetxController {
} }
} }
// 切换输入模式
void toggleInputMode() {
// 如果当前在图片输入模式,先清除
if (isImageInputActive.value) {
clearImageInput();
return;
}
isTextInputMode.toggle();
// 切换到语音模式时,直接开始语音输入
if (!isTextInputMode.value) {
startVoiceInput();
}
// 切换到文本模式时,直接停止语音输入
else {
stopVoiceInput();
}
}
// 从相册选择图片 // 从相册选择图片
Future<void> pickImage() async { Future<void> pickImage() async {
try { try {
@ -645,4 +756,13 @@ class AgentController extends GetxController {
_musiceManager.resume(); _musiceManager.resume();
isPlaying.value = true; isPlaying.value = true;
} }
// 修改自动停止处理,遵循TTS设置
void _handleAutoStop(int index) {
// 发送此消息到AI处理
isProcessing.value = true;
_isNewAssistantResponse = true;
AgentService.processTextInput(messages[index].text,
speakResponse: isTtsEnabled.value);
}
} }

260
lib/modules/agent/views/agent_view.dart

@ -10,42 +10,64 @@ class AgentView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final controller = Get.find<AgentController>(); final controller = Get.find<AgentController>();
final theme = Theme.of(context);
final primaryColor = theme.primaryColor;
final backgroundColor = theme.scaffoldBackgroundColor;
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
elevation: 1,
backgroundColor: backgroundColor,
leading: IconButton(
icon: Icon(Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black87,
size: 20),
onPressed: () => Get.back(),
),
titleSpacing: 0,
title: Row( title: Row(
children: [ children: [
Container( Container(
width: 40, width: 40,
height: 40, height: 40,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).primaryColor, color: primaryColor,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 3,
offset: const Offset(0, 1),
),
],
), ),
child: const Icon( child: const Icon(
Icons.smart_toy_outlined, Icons.smart_toy_outlined,
color: Colors.white, color: Colors.white,
size: 22,
), ),
), ),
const SizedBox(width: 10), const SizedBox(width: 12),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( Text(
'DeepVoice AI', 'DeepVoice AI',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black87,
), ),
), ),
Row( Row(
children: [ children: [
Container( Container(
width: 8, width: 6,
height: 8, height: 6,
decoration: BoxDecoration( decoration: const BoxDecoration(
color: Colors.green, color: Colors.green,
borderRadius: BorderRadius.circular(4), shape: BoxShape.circle,
), ),
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
@ -54,6 +76,7 @@ class AgentView extends StatelessWidget {
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Colors.green, color: Colors.green,
fontWeight: FontWeight.w500,
), ),
), ),
], ],
@ -63,14 +86,62 @@ class AgentView extends StatelessWidget {
], ],
), ),
actions: [ actions: [
PopupMenuButton( // TTS开关
icon: const Icon(Icons.more_vert), Obx(() => IconButton(
itemBuilder: (context) => [ icon: Icon(
PopupMenuItem( controller.isTtsEnabled.value
child: const Text('清除聊天记录'), ? Icons.volume_up
onTap: () => controller.clearChat(), : Icons.volume_off,
color: controller.isTtsEnabled.value
? primaryColor
: isDarkMode
? Colors.grey[400]
: Colors.grey,
),
onPressed: () => controller.toggleTts(),
)),
// 更多选项
Container(
width: 34,
height: 34,
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100],
shape: BoxShape.circle,
boxShadow: [
if (!isDarkMode)
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 2,
spreadRadius: 0.5,
),
],
),
child: PopupMenuButton(
icon: Icon(Icons.more_vert,
color: isDarkMode ? Colors.white : Colors.black87,
size: 20),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
), ),
], color: isDarkMode ? Colors.grey[800] : Colors.white,
itemBuilder: (context) => [
PopupMenuItem(
child: Row(
children: [
Icon(Icons.delete_outline, color: primaryColor, size: 20),
const SizedBox(width: 8),
const Text('清除聊天记录', style: TextStyle(fontSize: 14)),
],
),
onTap: () => Future.delayed(
const Duration(milliseconds: 100),
() => controller.clearChat(),
),
),
],
),
), ),
], ],
), ),
@ -81,19 +152,22 @@ class AgentView extends StatelessWidget {
child: Obx( child: Obx(
() => ListView.builder( () => ListView.builder(
controller: controller.scrollController, controller: controller.scrollController,
reverse: false, reverse: true,
padding: const EdgeInsets.all(16), padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
itemCount: controller.messages.length, itemCount: controller.messages.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final message = controller.messages[index]; final message = controller.messages[controller.messages.length - 1 - index];
return MessageBubble( return Padding(
isUser: message.isUser, padding: const EdgeInsets.only(bottom: 8),
message: message.text, child: MessageBubble(
timestamp: message.timestamp, isUser: message.isUser,
isRecognizing: message.isRecognizing, message: message.text,
hasImage: message.hasImage, timestamp: message.timestamp,
imagePath: message.imagePath, isRecognizing: message.isRecognizing,
mate: message.meta, hasImage: message.hasImage,
imagePath: message.imagePath,
mate: message.meta,
),
); );
}, },
), ),
@ -102,16 +176,22 @@ class AgentView extends StatelessWidget {
// 输入区域 // 输入区域
Container( Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor, color: backgroundColor,
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.05), color: Colors.black.withOpacity(0.06),
blurRadius: 5, blurRadius: 8,
offset: const Offset(0, -1), offset: const Offset(0, -1),
), ),
], ],
border: Border(
top: BorderSide(
color: Colors.grey.withOpacity(0.15),
width: 1,
),
),
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -141,12 +221,12 @@ class AgentView extends StatelessWidget {
child: GestureDetector( child: GestureDetector(
onTap: () => controller.clearImageInput(), onTap: () => controller.clearImageInput(),
child: Container( child: Container(
padding: EdgeInsets.all(4), padding: const EdgeInsets.all(4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black.withOpacity(0.6), color: Colors.black.withOpacity(0.6),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: Icon( child: const Icon(
Icons.close, Icons.close,
color: Colors.white, color: Colors.white,
size: 16, size: 16,
@ -158,7 +238,7 @@ class AgentView extends StatelessWidget {
), ),
// 预设提示选项 // 预设提示选项
SizedBox(height: 8), const SizedBox(height: 12),
SingleChildScrollView( SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Row( child: Row(
@ -168,13 +248,13 @@ class AgentView extends StatelessWidget {
label: '总结图片内容', label: '总结图片内容',
onTap: () => controller.summarizeImage(), onTap: () => controller.summarizeImage(),
), ),
SizedBox(width: 8), const SizedBox(width: 8),
_buildPromptChip( _buildPromptChip(
context: context, context: context,
label: '抽取图像文字', label: '抽取图像文字',
onTap: () => controller.extractTextFromImage(), onTap: () => controller.extractTextFromImage(),
), ),
SizedBox(width: 8), const SizedBox(width: 8),
_buildPromptChip( _buildPromptChip(
context: context, context: context,
label: '翻译图像文字', label: '翻译图像文字',
@ -183,11 +263,11 @@ class AgentView extends StatelessWidget {
], ],
), ),
), ),
SizedBox(height: 12), const SizedBox(height: 12),
], ],
); );
} else { } else {
return SizedBox.shrink(); return const SizedBox.shrink();
} }
}), }),
@ -202,15 +282,16 @@ class AgentView extends StatelessWidget {
controller.toggleInputMode(); controller.toggleInputMode();
}, },
child: Container( child: Container(
width: 48, width: 44,
height: 48, height: 44,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[200], color: Colors.grey[200],
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(22),
), ),
child: Icon( child: Icon(
isTextMode ? Icons.mic_none : Icons.keyboard, isTextMode ? Icons.mic_none : Icons.keyboard,
color: Colors.grey[600], color: Colors.grey[700],
size: 22,
), ),
), ),
); );
@ -229,7 +310,11 @@ class AgentView extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[200], color: Colors.grey[200],
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(22),
border: Border.all(
color: Colors.grey.withOpacity(0.1),
width: 1.0,
),
), ),
child: Row( child: Row(
children: [ children: [
@ -243,10 +328,18 @@ class AgentView extends StatelessWidget {
controller: controller.textController, controller: controller.textController,
maxLines: isImageActive ? 2 : 1, maxLines: isImageActive ? 2 : 1,
minLines: isImageActive ? 2 : 1, minLines: isImageActive ? 2 : 1,
style: TextStyle(
fontSize: 15,
color: isDarkMode ? Colors.white : Colors.black87,
),
decoration: InputDecoration( decoration: InputDecoration(
hintText: isImageActive hintText: isImageActive
? '添加对图片的问题或描述...' ? '添加对图片的问题或描述...'
: '输入消息...', : '输入消息...',
hintStyle: TextStyle(
color: Colors.grey[500],
fontSize: 15,
),
border: InputBorder.none, border: InputBorder.none,
contentPadding: EdgeInsets.symmetric( contentPadding: EdgeInsets.symmetric(
vertical: isImageActive ? 12 : 0), vertical: isImageActive ? 12 : 0),
@ -258,9 +351,13 @@ class AgentView extends StatelessWidget {
), ),
GestureDetector( GestureDetector(
onTap: () => controller.sendTextMessage(), onTap: () => controller.sendTextMessage(),
child: Icon( child: Container(
Icons.send, padding: const EdgeInsets.all(6),
color: Theme.of(context).primaryColor, child: Icon(
Icons.send,
color: primaryColor,
size: 22,
),
), ),
), ),
], ],
@ -277,12 +374,16 @@ class AgentView extends StatelessWidget {
} }
}, },
child: Container( child: Container(
height: 48, height: 44,
padding: padding:
const EdgeInsets.symmetric(horizontal: 16), const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[200], color: Colors.grey[200],
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(22),
border: Border.all(
color: Colors.grey.withOpacity(0.1),
width: 1.0,
),
), ),
child: Row( child: Row(
children: [ children: [
@ -306,19 +407,18 @@ class AgentView extends StatelessWidget {
Icons.mic, Icons.mic,
size: 14, size: 14,
color: isListening color: isListening
? Theme.of(context) ? primaryColor
.primaryColor : Colors.grey[500],
: Colors.grey,
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
'正在聆听', '正在聆听',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 13,
fontWeight: FontWeight.w500,
color: isListening color: isListening
? Theme.of(context) ? primaryColor
.primaryColor : Colors.grey[500],
: Colors.grey,
), ),
), ),
], ],
@ -339,18 +439,22 @@ class AgentView extends StatelessWidget {
// 图片选择按钮 // 图片选择按钮
Container( Container(
width: 48, width: 44,
height: 48, height: 44,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[200], color: Colors.grey[200],
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(22),
), ),
child: PopupMenuButton<String>( child: PopupMenuButton<String>(
icon: Icon( icon: Icon(
Icons.add_photo_alternate_outlined, Icons.add_photo_alternate_outlined,
color: Colors.grey[600], color: Colors.grey[700],
size: 22,
), ),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
onSelected: (value) async { onSelected: (value) async {
if (value == 'camera') { if (value == 'camera') {
await controller.takePhoto(); await controller.takePhoto();
@ -364,9 +468,9 @@ class AgentView extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
Icon(Icons.camera_alt, Icon(Icons.camera_alt,
color: Theme.of(context).primaryColor), color: primaryColor, size: 20),
SizedBox(width: 8), const SizedBox(width: 8),
Text('拍照'), const Text('拍照', style: TextStyle(fontSize: 14)),
], ],
), ),
), ),
@ -375,9 +479,9 @@ class AgentView extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
Icon(Icons.photo_library, Icon(Icons.photo_library,
color: Theme.of(context).primaryColor), color: primaryColor, size: 20),
SizedBox(width: 8), const SizedBox(width: 8),
Text('从相册选择'), const Text('从相册选择', style: TextStyle(fontSize: 14)),
], ],
), ),
), ),
@ -400,22 +504,26 @@ class AgentView extends StatelessWidget {
required String label, required String label,
required VoidCallback onTap, required VoidCallback onTap,
}) { }) {
final theme = Theme.of(context);
final primaryColor = theme.primaryColor;
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
child: Container( child: Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withOpacity(0.1), color: primaryColor.withOpacity(0.08),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: Border.all( border: Border.all(
color: Theme.of(context).primaryColor.withOpacity(0.3), color: primaryColor.withOpacity(0.2),
width: 1,
), ),
), ),
child: Text( child: Text(
label, label,
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: Theme.of(context).primaryColor, color: primaryColor,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
@ -463,7 +571,7 @@ class _SoundBarState extends State<_SoundBar>
duration: const Duration(milliseconds: 500), duration: const Duration(milliseconds: 500),
); );
_animation = Tween<double>(begin: 3, end: 15).animate( _animation = Tween<double>(begin: 3, end: 18).animate(
CurvedAnimation( CurvedAnimation(
parent: _controller, parent: _controller,
curve: Curves.easeInOut, curve: Curves.easeInOut,
@ -493,7 +601,7 @@ class _SoundBarState extends State<_SoundBar>
} else { } else {
_controller.reset(); _controller.reset();
// 设置为小高度的静态状态 // 设置为小高度的静态状态
_controller.animateTo(0.1, duration: Duration(milliseconds: 300)); _controller.animateTo(0.1, duration: const Duration(milliseconds: 300));
} }
} }
@ -505,13 +613,17 @@ class _SoundBarState extends State<_SoundBar>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final primaryColor = Theme.of(context).primaryColor;
return Container( return Container(
width: 2, width: 2.5,
height: widget.isActive ? _animation.value : 3, height: widget.isActive ? _animation.value : 3,
margin: const EdgeInsets.symmetric(horizontal: 1),
decoration: BoxDecoration( decoration: BoxDecoration(
color: color: widget.isActive
widget.isActive ? Theme.of(context).primaryColor : Colors.grey[400], ? primaryColor.withOpacity(0.8)
borderRadius: BorderRadius.circular(1), : Colors.grey.withOpacity(0.4),
borderRadius: BorderRadius.circular(1.5),
), ),
); );
} }

4
lib/modules/agent/views/message_bubble.dart

@ -197,7 +197,7 @@ class MessageBubble extends GetView<AgentController> {
// 判断消息内容,显示特殊卡片 // 判断消息内容,显示特殊卡片
if (!isUser && mate != null) { if (!isUser && mate != null) {
if (mate!.keys.contains('card_music')) { if (mate!.keys.contains('card_music')) {
final musicId = mate!['card_music']['id'] as String; final musicId = mate!['card_music']['id'].toString();
final musicName = mate!['card_music']['name'] as String; final musicName = mate!['card_music']['name'] as String;
final musicUrl = mate!['card_music']['url'] as String; final musicUrl = mate!['card_music']['url'] as String;
final musicSinger = mate!['card_music']['sgener'] as String; final musicSinger = mate!['card_music']['sgener'] as String;
@ -326,7 +326,7 @@ class MessageBubble extends GetView<AgentController> {
...playlist.asMap().entries.map((entry) { ...playlist.asMap().entries.map((entry) {
final index = entry.key; final index = entry.key;
final item = entry.value; final item = entry.value;
final id = item["id"] ?? ""; final id = item["id"]?.toString() ?? "";
final name = item["name"] ?? "未知歌曲"; final name = item["name"] ?? "未知歌曲";
final url = item["url"] ?? ""; final url = item["url"] ?? "";
final singer = item["sgener"] ?? "未知歌手"; final singer = item["sgener"] ?? "未知歌手";

11
lib/modules/meeting/bindings/meeting_edit_binding.dart

@ -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>(() => MeetingEditController());
}
}

10
lib/modules/meeting/bindings/meeting_record_binding.dart

@ -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>(() => MeetingRecordController());
}
}

79
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/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:get/get.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'; import '../../../data/services/db/sqflite_api.dart';
class MeetingController extends GetxController { class MeetingController extends GetxController {
final ScrollController listController = ScrollController(); final ScrollController listController = ScrollController();
final AudioPlayer _audioPlayer = AudioPlayer();
// 是否还有更多数据 // 是否还有更多数据
bool isLoadingMore = true; bool isLoadingMore = true;
@ -18,6 +27,7 @@ class MeetingController extends GetxController {
void onInit() { void onInit() {
super.onInit(); super.onInit();
listController.addListener(_onScroll); listController.addListener(_onScroll);
Permission.storage.request();
_getDataList(); _getDataList();
} }
@ -25,6 +35,7 @@ class MeetingController extends GetxController {
void onClose() { void onClose() {
listController.dispose(); listController.dispose();
titleController.dispose(); titleController.dispose();
_audioPlayer.dispose();
super.onClose(); 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<String, Object?> 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<String> _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 { void editMeetingTitle(int id, String title) async {
if (title.isEmpty) return; if (title.isNotEmpty) {
SqfliteApi.editMeetingTitle(id, title); SqfliteApi.editMeetingTitle(id, title);
int index = dataList.indexWhere((item) => item['id'] == id); int index = dataList.indexWhere((item) => item['id'] == id);
if (index != -1) { if (index != -1) {
final dataMap = Map<String, dynamic>.from(dataList[index]); final dataMap = Map<String, dynamic>.from(dataList[index]);
dataMap['title'] = title; dataMap['title'] = title;
dataList[index] = dataMap; dataList[index] = dataMap;
}
} }
Get.back(); Get.back();
} }
// 删除数据 // 删除数据
void deleteMeeting(int id) { void deleteMeeting(int id, String filepath) async {
dataList.removeWhere((item) => item['id'] == id); dataList.removeWhere((item) => item['id'] == id);
// 删除数据库中的记录 // 删除数据库中的记录
SqfliteApi.deleteMeeting(id); SqfliteApi.deleteMeeting(id);
_deleteFile(filepath);
}
// 删除文件
Future<void> _deleteFile(String filepath) async {
File file = File(filepath);
bool fileExists = await file.exists();
if (fileExists) {
await file.delete();
}
} }
} }

361
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/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:get/get.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/db/sqflite_api.dart';
import '../../../data/services/open_ai_service_adapter.dart';
import '../model/meeting_details_model.dart'; import '../model/meeting_details_model.dart';
import '../model/meeting_model.dart'; import '../model/meeting_model.dart';
class MeetingDetailsController extends GetxController class MeetingDetailsController extends GetxController
with GetSingleTickerProviderStateMixin { with GetSingleTickerProviderStateMixin {
late AnimationController lottieController; late ScrollController scrollController;
final AudioPlayer audioPlayer = AudioPlayer(); late TabController tabController;
final RxInt currentIndex = 0.obs;
final PlayerController playerController = PlayerController();
RxBool isTop = false.obs;
RxInt durationMax = 0.obs;
int id = 0; int id = 0;
RxBool isLoading = true.obs; RxBool isLoading = true.obs;
Rx<MeetingModel> meetingData = MeetingModel().obs; Rx<MeetingModel> meetingData = MeetingModel().obs;
Rx<MeetingDetailsModel> dataDetails = MeetingDetailsModel().obs; Rx<MeetingDetailsModel> 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<OpenAIServiceAdapter>();
// 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<String, String> _headers = {};
@override @override
void onInit() { void onInit() {
super.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<String, dynamic> args = Get.arguments ?? {}; final Map<String, dynamic> args = Get.arguments ?? {};
id = args['id']; id = args['id'];
_headers = {
'Authorization': 'Bearer; $_token',
'Content-Type': 'application/json',
};
_getAgent();
_getDataDetails(); _getDataDetails();
} }
@override @override
void onClose() { void onClose() {
lottieController.dispose(); scrollController.dispose();
audioPlayer.dispose(); tabController.dispose();
playerController.dispose();
_aiResponseSubscription?.cancel();
_isDisposed = true;
super.onClose(); 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<DBAgent> 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 { void _getDataDetails() async {
final data = await SqfliteApi.getMeetingDetails(id); final data = await SqfliteApi.getMeetingDetails(id);
meetingData.value = MeetingModel.fromJson(data); meetingData.value = MeetingModel.fromJson(data);
dataDetails.value = MeetingDetailsModel.fromJson(data['details']); meetingDetails.value = MeetingDetailsModel.fromJson(data['details']);
audioPlayer.setAudioSource(AudioSource.file(dataDetails.value.audio)); await playerController.preparePlayer(
audioPlayer.playerStateStream.listen((state) async { path: meetingData.value.filepath,
if (state.processingState == ProcessingState.completed) { noOfSamples: 1200,
await audioPlayer.seek(Duration.zero); );
audioPlayer.pause(); durationMax.value = await playerController.getDuration(DurationType.max);
lottieController.stop(); playerController.setFinishMode(finishMode: FinishMode.pause);
isPlay.value = false; if (meetingDetails.value.tasktype == 1) {
} List speakerList = await SqfliteApi.getMeetingSpeaker(id);
}); textList.value = List.from(speakerList);
}
isLoading.value = false; isLoading.value = false;
} }
void start() async { void editSpeakerText() async {
if (isPlay.value) { if (editSpeakerTextId.value != 0 && editSpeakerTextChange.isNotEmpty) {
lottieController.stop(); Map? foundElement = editSpeakerTextList.firstWhere(
audioPlayer.pause(); (element) => element['id'] == editSpeakerTextId.value,
isPlay.value = false; 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<String, dynamic>.from(textList[index]);
dataMap['content'] = element['content'];
textList[index] = dataMap;
}
}
isSpeakerText.value = false;
editSpeakerTextId.value = 0;
editSpeakerTextList = [];
}
EasyLoading.dismiss();
} else { } else {
lottieController.repeat(); isSpeakerText.value = false;
audioPlayer.play(); editSpeakerTextId.value = 0;
isPlay.value = true; 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<void> _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<String> _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<String> array = path.split('.');
return array[array.length - 1];
}
Future<Map<String, dynamic>> _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<void> _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<void> _addTextList(List utterances) async {
String text = '';
List<Map<String, Object?>> 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;
} }
} }

143
lib/modules/meeting/controllers/meeting_edit_controller.dart

@ -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<MeetingController>();
final MeetingDetailsController meetingDetailsController =
Get.find<MeetingDetailsController>();
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<String, dynamic> 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<VolcanoAIService>();
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<String, Object?> 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<String, dynamic>.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();
}
}

324
lib/modules/meeting/controllers/meeting_record_controller.dart

@ -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<MeetingController>();
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<XunfeiAsrService>();
final Map<String, dynamic> 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<void> _pause() async {
await _stopRecorder();
lottieController.stop();
isRecording.value = false;
timer?.cancel();
}
//开始录音
Future<void> _startRecorder() async {
final recognitionStream =
await _asrService.startContinuousRecognition(false);
_recognitionSubscription = recognitionStream.listen(
_handleRecognitionEvent,
);
}
//结束录音
Future<void> _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<VolcanoAIService>();
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<String, Object?> 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<String> _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;
}
}

30
lib/modules/meeting/model/meeting_details_model.dart

@ -3,25 +3,37 @@ class MeetingDetailsModel {
late int id; late int id;
late int meetingid; late int meetingid;
late String audio; late String address;
late String content; late String personnel;
late String aisummary; late String taskid;
late int tasktype;
late String overview;
late String summary;
late String mindmap;
factory MeetingDetailsModel.fromJson(Map<String, dynamic> map) => factory MeetingDetailsModel.fromJson(Map<String, dynamic> map) =>
MeetingDetailsModel() MeetingDetailsModel()
..id = map["id"] ..id = map["id"]
..meetingid = map["meetingid"] ..meetingid = map["meetingid"]
..audio = map["audio"] ..address = map["address"]
..content = map["content"] ..personnel = map["personnel"]
..aisummary = map["aisummary"]; ..taskid = map["taskid"]
..tasktype = map["tasktype"]
..overview = map["overview"]
..summary = map["summary"]
..mindmap = map["mindmap"];
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final data = <String, dynamic>{}; final data = <String, dynamic>{};
data['id'] = id; data['id'] = id;
data['meetingid'] = meetingid; data['meetingid'] = meetingid;
data['audio'] = audio; data['address'] = address;
data['content'] = content; data['personnel'] = personnel;
data['aisummary'] = aisummary; data['taskid'] = taskid;
data['tasktype'] = tasktype;
data['overview'] = overview;
data['summary'] = summary;
data['mindmap'] = mindmap;
return data; return data;
} }
} }

21
lib/modules/meeting/model/meeting_model.dart

@ -3,32 +3,29 @@ class MeetingModel {
late int id; late int id;
late String title; late String title;
late String content; late String type;
late int seconds; late int seconds;
late String address; late String filepath;
late String longitude; late String audiourl;
late String latitude;
late int creationtime; late int creationtime;
factory MeetingModel.fromJson(Map<String, dynamic> map) => MeetingModel() factory MeetingModel.fromJson(Map<String, dynamic> map) => MeetingModel()
..id = map["id"] ..id = map["id"]
..title = map["title"] ..title = map["title"]
..content = map["content"] ..type = map["type"]
..seconds = map["seconds"] ..seconds = map["seconds"]
..address = map["address"] ..filepath = map["filepath"]
..longitude = map["longitude"] ..audiourl = map["audiourl"]
..latitude = map["latitude"]
..creationtime = map["creationtime"]; ..creationtime = map["creationtime"];
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final data = <String, dynamic>{}; final data = <String, dynamic>{};
data['id'] = id; data['id'] = id;
data['title'] = title; data['title'] = title;
data['content'] = content; data['type'] = type;
data['seconds'] = seconds; data['seconds'] = seconds;
data['address'] = address; data['filepath'] = filepath;
data['longitude'] = longitude; data['audiourl'] = audiourl;
data['latitude'] = latitude;
data['creationtime'] = creationtime; data['creationtime'] = creationtime;
return data; return data;
} }

566
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:common_utils/common_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:lottie/lottie.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../routes/app_routes.dart';
import '../controllers/meeting_details_controller.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<MeetingDetailsController> { class MeetingDetailsView extends GetView<MeetingDetailsController> {
const MeetingDetailsView({super.key}); const MeetingDetailsView({super.key});
@ -15,313 +18,356 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark; final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold( return Obx(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], () => Scaffold(
appBar: _buildAppBar(isDarkMode), backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
body: Obx(() => !controller.isLoading.value appBar: _buildAppBar(isDarkMode),
? _buildBody(isDarkMode) body: NestedScrollView(
: const SizedBox()), 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<int>(
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<PlayerState>(
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) { PreferredSizeWidget _buildAppBar(bool isDarkMode) {
return AppBar( return AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white, backgroundColor:
controller.isTop.value ? Colors.grey[50] : Colors.grey[200],
surfaceTintColor: Colors.transparent,
elevation: 0.5, elevation: 0.5,
leading: IconButton( leading: controller.isSpeakerText.value
icon: Icon( ? GestureDetector(
Icons.arrow_back_ios, onTap: () {
color: isDarkMode ? Colors.white : Colors.black, controller.isSpeakerText.value = false;
size: 20.sp, controller.editSpeakerTextId.value = 0;
), controller.editSpeakerTextList = [];
onPressed: () => Get.back(), },
), child: Center(
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(
child: Text( child: Text(
controller.meetingData.value.title, 'cancel'.tr,
style: TextStyle( style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87, color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 17.sp, fontSize: 16.sp,
), ),
), ),
), ),
10.horizontalSpace, )
Text( : IconButton(
DateUtil.formatDateMs( icon: Icon(
controller.meetingData.value.creationtime, Icons.arrow_back_ios,
format: DateFormats.y_mo_d_h_m, color: isDarkMode ? Colors.white : Colors.black,
), size: 20.sp,
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 14.sp,
),
), ),
], onPressed: () => Get.back(),
),
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,
),
),
],
), ),
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( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
StreamBuilder<Duration>( StreamBuilder<PlayerState>(
stream: controller.audioPlayer.positionStream, stream: controller.playerController.onPlayerStateChanged,
builder: (context, snapshot) { builder: (context, snapshot) {
final position = snapshot.data ?? Duration.zero; bool isPlay = snapshot.data?.isPlaying ?? false;
return Text( return GestureDetector(
DateUtil.formatDateMs( onTap: () {
position.inMilliseconds, if (isPlay) {
format: 'HH:mm:ss', controller.playerController.pausePlayer();
isUtc: true, } else {
), controller.playerController.startPlayer();
style: TextStyle( }
color: isDarkMode ? Colors.grey[400] : Colors.grey[500], },
fontSize: 12.sp, child: Icon(
isPlay ? Icons.pause_circle : Icons.play_circle,
color: Colors.black,
size: 30.w,
), ),
); );
}, },
), ),
GestureDetector( 4.horizontalSpace,
onTap: controller.start, AudioFileWaveforms(
child: Container( size: Size(130.w, 30.w),
width: 30.w, playerController: controller.playerController,
height: 30.w, playerWaveStyle: PlayerWaveStyle(
decoration: BoxDecoration( fixedWaveColor: Colors.grey[400]!,
color: isDarkMode ? AppColors.primary : Colors.blue, liveWaveColor: Colors.red,
borderRadius: BorderRadius.circular( seekLineColor: Colors.red,
12.w, scaleFactor: 100,
),
),
child: Icon(
controller.isPlay.value ? Icons.pause : Icons.play_arrow,
color: Colors.white,
size: 25.w,
),
), ),
), ),
StreamBuilder<Duration?>(
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) { Widget _buildBody(BuildContext context, bool isDarkMode) {
return Expanded( return Column(
child: Container( children: [
width: double.infinity, Container(
padding: EdgeInsets.all(10.w), padding: EdgeInsets.symmetric(horizontal: 8.w),
margin: EdgeInsets.symmetric(vertical: 5.w), margin: EdgeInsets.symmetric(vertical: 10.w),
decoration: BoxDecoration( child: Obx(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, () => Row(
borderRadius: BorderRadius.circular(12.w), children: [
boxShadow: [ _iconItem(Icons.summarize, '概述', 0),
BoxShadow( _iconItem(Icons.message, '转写', 1),
color: isDarkMode _iconItem(Icons.article, '总结', 2),
? Colors.black.withOpacity(0.2) _iconItem(Icons.lan, '思维导图', 3),
: Colors.black.withOpacity(0.05), ],
blurRadius: 10,
offset: const Offset(0, 2),
), ),
], ),
), ),
child: DefaultTabController( Expanded(
length: 2, child: Padding(
child: Column( padding: EdgeInsets.symmetric(horizontal: 12.w),
children: [ child: TabBarView(
SizedBox( controller: controller.tabController,
height: 40.w, physics: const NeverScrollableScrollPhysics(),
child: TabBar( children: const [
indicatorSize: TabBarIndicatorSize.tab, OverviewTab(),
indicatorColor: isDarkMode ? AppColors.primary : Colors.blue, SpeechTab(),
labelColor: isDarkMode ? AppColors.primary : Colors.blue, SummaryTab(),
labelStyle: TextStyle( MindMapTab(),
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,
),
),
],
),
),
],
), ),
), ),
), ],
); );
} }
// 底部按钮 Widget _iconItem(IconData icon, String title, int index) {
Widget _bottomButton(bool isDarkMode) { bool isActive = controller.currentIndex.value == index;
return Container(
padding: EdgeInsets.only(top: 20.w, bottom: 10.w), return Expanded(
alignment: Alignment.center, flex: isActive ? 1 : 0,
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
Get.toNamed(Routes.meetingEdit, arguments: { if (isActive) return;
'id': controller.id, controller.tabController.index = index;
}); controller.isSpeakerText.value = false;
controller.editSpeakerTextId.value = 0;
controller.editSpeakerTextList = [];
}, },
child: Container( child: Container(
width: 130.w, height: 45.w,
height: 48.w, padding: EdgeInsets.symmetric(horizontal: 18.w),
margin: EdgeInsets.symmetric(horizontal: 4.w),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode ? AppColors.primary : Colors.blue, color: isActive ? Colors.black : Colors.grey[200],
borderRadius: BorderRadius.circular(12.w), borderRadius: BorderRadius.circular(12.w),
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( Icon(
Icons.edit_calendar, icon,
color: Colors.white, color: isActive ? Colors.white : Colors.grey[600],
size: 20.w, size: 20.sp,
),
5.horizontalSpace,
Text(
'编辑',
style: TextStyle(color: Colors.white, fontSize: 16.sp),
), ),
if (isActive)
Padding(
padding: EdgeInsets.only(left: 5.w),
child: Text(
title,
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
),
),
),
], ],
), ),
), ),

301
lib/modules/meeting/views/meeting_edit_view.dart

@ -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<MeetingEditController> {
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<Duration>(
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<Duration?>(
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,
),
),
),
);
}
}

363
lib/modules/meeting/views/meeting_record_view.dart

@ -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<MeetingRecordController> {
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();
}
}
}

322
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/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:haptic_feedback/haptic_feedback.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
@ -19,6 +20,10 @@ class MeetingView extends GetView<MeetingController> {
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: _buildAppBar(isDarkMode), appBar: _buildAppBar(isDarkMode),
body: _buildBody(context, isDarkMode), body: _buildBody(context, isDarkMode),
floatingActionButton: FloatingActionButton(
onPressed: controller.addAudio,
child: const Icon(Icons.add),
),
); );
} }
@ -49,50 +54,15 @@ class MeetingView extends GetView<MeetingController> {
// 构建主体内容 // 构建主体内容
Widget _buildBody(BuildContext context, bool isDarkMode) { Widget _buildBody(BuildContext context, bool isDarkMode) {
return Stack( return Obx(
children: [ () => ListView.builder(
Obx( itemCount: controller.dataList.length,
() => ListView.builder( padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w),
itemCount: controller.dataList.length, itemBuilder: (BuildContext context, int index) {
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w), MeetingModel data = MeetingModel.fromJson(controller.dataList[index]);
itemBuilder: (BuildContext context, int index) { return _item(context, isDarkMode, data);
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),
),
],
),
),
),
),
],
); );
} }
@ -103,52 +73,11 @@ class MeetingView extends GetView<MeetingController> {
'id': data.id, 'id': data.id,
}); });
}, },
onLongPressStart: (details) { onLongPressStart: (LongPressStartDetails details) {
RenderBox? renderBox = _onLongPressItem(context, isDarkMode, details, data);
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);
},
),
],
);
}, },
child: Container( 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), margin: EdgeInsets.symmetric(vertical: 5.w),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
@ -164,89 +93,67 @@ class MeetingView extends GetView<MeetingController> {
], ],
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Text(
children: [ data.title,
Expanded( style: TextStyle(
child: Text( color: isDarkMode ? Colors.white : Colors.black87,
data.title, 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( style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87, color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 16.sp, fontSize: 12.sp,
), ),
overflow: TextOverflow.ellipsis,
), ),
), 20.horizontalSpace,
10.horizontalSpace, Icon(
Text( Icons.access_time,
DateUtil.formatDateMs( size: 14.w,
data.creationtime, color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
format: DateFormats.y_mo_d_h_m,
), ),
style: TextStyle( 2.horizontalSpace,
color: isDarkMode ? Colors.white70 : Colors.black54, Text(
fontSize: 12.sp, '${data.seconds}s',
), style: TextStyle(
), color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
], 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,
), ),
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<MeetingController> {
); );
} }
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( PopupMenuEntry _popupMenuEntry(
Icon icon, Icon icon,
String title, String title,
@ -263,14 +222,17 @@ class MeetingView extends GetView<MeetingController> {
return PopupMenuItem( return PopupMenuItem(
onTap: onTap, onTap: onTap,
child: SizedBox( child: SizedBox(
width: 120.w, width: 140.w,
child: Row( child: Row(
children: [ children: [
icon, icon,
8.horizontalSpace, 8.horizontalSpace,
Text( Text(
title, title,
style: TextStyle(color: color), style: TextStyle(
color: color,
fontSize: 14.sp,
),
), ),
], ],
), ),
@ -280,9 +242,9 @@ class MeetingView extends GetView<MeetingController> {
void _bottomSheet( void _bottomSheet(
BuildContext context, BuildContext context,
bool isDarkMode, { bool isDarkMode,
int? id, int id,
}) { ) {
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
@ -298,7 +260,7 @@ class MeetingView extends GetView<MeetingController> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
id != null ? 'editMeetingTitle'.tr : 'meetingTitle'.tr, // 编辑会议标题 'editMeetingTitle'.tr, // 编辑会议标题
style: TextStyle( style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87, color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 16.sp, fontSize: 16.sp,
@ -363,7 +325,7 @@ class MeetingView extends GetView<MeetingController> {
borderRadius: BorderRadius.circular(12.w), borderRadius: BorderRadius.circular(12.w),
), ),
child: Text( child: Text(
'cancel'.tr,// 取消 'cancel'.tr, // 取消
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: color:
@ -374,20 +336,8 @@ class MeetingView extends GetView<MeetingController> {
), ),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
int time = DateTime.now().millisecondsSinceEpoch;
String title = controller.titleController.text; String title = controller.titleController.text;
if (id != null) { controller.editMeetingTitle(id, title);
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,
});
}
}, },
child: Container( child: Container(
width: 120.w, width: 120.w,
@ -398,7 +348,7 @@ class MeetingView extends GetView<MeetingController> {
borderRadius: BorderRadius.circular(12.w), borderRadius: BorderRadius.circular(12.w),
), ),
child: Text( child: Text(
'confirm'.tr,// 确认 'confirm'.tr, // 确认
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 14.sp, fontSize: 14.sp,
@ -414,8 +364,6 @@ class MeetingView extends GetView<MeetingController> {
), ),
); );
}, },
).then((value) { );
controller.titleController.text = '';
});
} }
} }

12
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(''),
);
}
}

12
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(''),
);
}
}

202
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),
),
),
);
}
}

256
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<MeetingDetailsController> {
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,
),
),
],
),
),
);
}
}

226
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<MeetingDetailsController> {
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,
),
),
],
),
),
);
}
}

5
lib/modules/pairing/controllers/pairing_controller.dart

@ -186,7 +186,7 @@ class PairingController extends GetxController {
if (Platform.isAndroid) { if (Platform.isAndroid) {
permissionsToRequest.add(Permission.locationWhenInUse); permissionsToRequest.add(Permission.locationWhenInUse);
permissionsToRequest.add(Permission.microphone); // permissionsToRequest.add(Permission.microphone);
if (await _getAndroidSdkVersion() >= 31) { if (await _getAndroidSdkVersion() >= 31) {
permissionsToRequest.add(Permission.bluetoothScan); permissionsToRequest.add(Permission.bluetoothScan);
permissionsToRequest.add(Permission.bluetoothConnect); permissionsToRequest.add(Permission.bluetoothConnect);
@ -235,6 +235,9 @@ class PairingController extends GetxController {
} }
_updateStatusDescription(); _updateStatusDescription();
}); });
if (needdeivice) {
_handleDeviceConnected();
}
} }
/// 处理设备连接成功事件 /// 处理设备连接成功事件

18
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/explore/bindings/explore_binding.dart';
import '../modules/meeting/bindings/meeting_binding.dart'; import '../modules/meeting/bindings/meeting_binding.dart';
import '../modules/meeting/bindings/meeting_details_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_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/meeting/views/meeting_view.dart';
import '../modules/profile/views/profile_view.dart'; import '../modules/profile/views/profile_view.dart';
import '../modules/profile/bindings/profile_binding.dart'; import '../modules/profile/bindings/profile_binding.dart';
@ -138,13 +134,6 @@ abstract class AppPages {
transition: Transition.rightToLeft, transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250), transitionDuration: Duration(milliseconds: 250),
), ),
GetPage(
name: Routes.meetingRecord,
page: () => const MeetingRecordView(),
binding: MeetingRecordBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage( GetPage(
name: Routes.meetingDetails, name: Routes.meetingDetails,
page: () => const MeetingDetailsView(), page: () => const MeetingDetailsView(),
@ -152,13 +141,6 @@ abstract class AppPages {
transition: Transition.rightToLeft, transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250), transitionDuration: Duration(milliseconds: 250),
), ),
GetPage(
name: Routes.meetingEdit,
page: () => const MeetingEditView(),
binding: MeetingEditBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage( GetPage(
name: Routes.timbre, name: Routes.timbre,
page: () => const TimbreView(), page: () => const TimbreView(),

4
lib/routes/app_routes.dart

@ -20,9 +20,7 @@ abstract class Routes {
// 会议相关 // 会议相关
static const meeting = '/meeting'; static const meeting = '/meeting';
static const meetingRecord = '/meeting_record'; // 会议记录 static const meetingDetails = '/meetingDetails';
static const meetingDetails = '/meeting_details'; // 会议详情
static const meetingEdit = '/meeting_edit'; // 会议编辑
// 语言功能 // 语言功能
static const translation = '/translation'; // 实时翻译 static const translation = '/translation'; // 实时翻译

3
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -666,12 +666,11 @@ object AgentService : CoroutineScope {
override fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) { override fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) {
audioPlayer?.stopAudio() audioPlayer?.stopAudio()
FileLogger.d(TAG, "mcp调用结果: $functionCallResult")
sendEvent("function_call_result", mapOf( sendEvent("function_call_result", mapOf(
"function_call" to functionCall.toString(), "function_call" to functionCall.toString(),
"result" to functionCallResult.toString(), "result" to functionCallResult.toString(),
)) ))
FileLogger.d(TAG, "mcp调用结果: $functionCallResult") FileLogger.d(TAG, "mcp调用结果: $functionCallResult")
metadata = autoPlayMusic(functionCallResult); metadata = autoPlayMusic(functionCallResult);
} }
} }

5
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

@ -606,9 +606,8 @@ class AzureAsrHelper(private val context: Context) {
val sessionId = audioRecord?.audioSessionId ?: return val sessionId = audioRecord?.audioSessionId ?: return
// 回音消除 // 回音消除
echoCanceler = AcousticEchoCanceler.create(sessionId).apply { echoCanceler = AcousticEchoCanceler.create(sessionId)
enabled = true echoCanceler?.enabled = true;
}
} }
/** /**

11
local_plugins/music_service/android/src/main/AndroidManifest.xml

@ -7,20 +7,19 @@
<application> <application>
<service <service
android:name=".MusicService" android:name=".MusicService"
android:exported="true"> android:exported="true"
android:foregroundServiceType="mediaPlayback"
>
<!-- 让系统知道这是媒体浏览服务 --> <!-- 让系统知道这是媒体浏览服务 -->
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
<!-- 让系统知道这里能处理媒体按键 -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" /> <action android:name="android.intent.action.MEDIA_BUTTON" />
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter> </intent-filter>
</service> </service>
<receiver <receiver
android:name="androidx.media.session.MediaButtonReceiver" android:name="androidx.media.session.MediaButtonReceiver"
android:exported="false"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" /> <action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter> </intent-filter>

2
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt

@ -55,6 +55,7 @@ class MusicService : MediaBrowserServiceCompat() {
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
FileLogger.i(TAG, "onStartCommand: $intent")
intent?.action?.takeIf { it == Intent.ACTION_MEDIA_BUTTON }?.let { intent?.action?.takeIf { it == Intent.ACTION_MEDIA_BUTTON }?.let {
// 分发给 MediaSession 处理媒体按钮 // 分发给 MediaSession 处理媒体按钮
MediaButtonReceiver.handleIntent(mediaSession, intent) MediaButtonReceiver.handleIntent(mediaSession, intent)
@ -62,7 +63,6 @@ class MusicService : MediaBrowserServiceCompat() {
} }
val command = intent?.getStringExtra("command") ?: return START_NOT_STICKY val command = intent?.getStringExtra("command") ?: return START_NOT_STICKY
FileLogger.i(TAG, "liwei------------------- onStartCommand: $command")
when (command) { when (command) {
"play" -> intent.getSerializableExtra("song")?.let { url -> "play" -> intent.getSerializableExtra("song")?.let { url ->
val songMap = intent.getSerializableExtra("song") as? Map<String, String> val songMap = intent.getSerializableExtra("song") as? Map<String, String>

1
local_plugins/open_ai_service/android/build.gradle.kts

@ -60,4 +60,5 @@ dependencies {
implementation("io.ktor:ktor-client-logging:3.1.2") implementation("io.ktor:ktor-client-logging:3.1.2")
implementation("com.google.code.gson:gson:2.10.1") implementation("com.google.code.gson:gson:2.10.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
implementation("androidx.media:media:1.6.0")
} }

2
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt

@ -718,7 +718,7 @@ class MCPClient(private val context: Context? = null) : AutoCloseable {
*/ */
suspend fun callTool(name: String, arguments: Map<String, Any>): JSONObject? { suspend fun callTool(name: String, arguments: Map<String, Any>): JSONObject? {
val requestId = "${System.currentTimeMillis()}-${(0..1000).random()}" val requestId = "${System.currentTimeMillis()}-${(0..1000).random()}"
Log.d(TAG, "[$requestId] 调用工具: $name") Log.d(TAG, "[$requestId] 调用工具: $name 参数:$arguments")
val toolType = getToolType(name) val toolType = getToolType(name)

6
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt

@ -528,7 +528,7 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope {
val call = client.newCall(request) val call = client.newCall(request)
// 保存当前Call对象以便后续取消 // 保存当前Call对象以便后续取消
currentStreamCall = call currentStreamCall = call
Log.d(TAG, "发送消息流式输出: request=$request") // Log.d(TAG, "发送消息流式输出: request=$request")
call.enqueue(object : Callback { call.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) { override fun onFailure(call: Call, e: IOException) {
Log.d(TAG, "发送消息流式输出失败: e=$e") Log.d(TAG, "发送消息流式输出失败: e=$e")
@ -638,7 +638,7 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope {
// 处理普通文本内容 // 处理普通文本内容
if (delta.has("content")) { if (delta.has("content")) {
val content = delta.getString("content") val content = delta.getString("content")
Log.d(TAG, "处理流式JSON数据: content=$content") // Log.d(TAG, "处理流式JSON数据: content=$content")
if (!isCanceled) { if (!isCanceled) {
callback.onToken(content) callback.onToken(content)
} }
@ -698,7 +698,7 @@ class OpenAIService(private val context: Context? = null) : CoroutineScope {
put("arguments", firstToolCall.arguments) put("arguments", firstToolCall.arguments)
put("id", firstToolCall.id) put("id", firstToolCall.id)
} }
Log.d(TAG, "工具调用: id=${firstToolCall.id}, name=${firstToolCall.name}") Log.d(TAG, "工具调用: id=${firstToolCall.id}, name=${firstToolCall.name} arguments=${firstToolCall.arguments}")
// 通知上层工具调用事件 // 通知上层工具调用事件
callback.onFunctionCall(functionCall) callback.onFunctionCall(functionCall)

31
local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/SystemFunctionHandler.kt

@ -17,7 +17,7 @@ import android.util.Log
import org.json.JSONObject import org.json.JSONObject
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume import kotlin.coroutines.resume
import android.view.KeyEvent
/** /**
* 系统功能处理器 - 负责调用系统API执行各种功能以及注册系统功能 * 系统功能处理器 - 负责调用系统API执行各种功能以及注册系统功能
*/ */
@ -430,27 +430,22 @@ class SystemFunctionHandler(private val context: Context) {
) )
flags = Intent.FLAG_RECEIVER_FOREGROUND flags = Intent.FLAG_RECEIVER_FOREGROUND
} }
context.sendOrderedBroadcast(keyIntent, null) keyIntent.setPackage("com.yunqiinnovation.deep_voice")
context.sendBroadcast(keyIntent)
return Result( return Result(
success = true, success = true,
message = "已开始播放媒体" message = "已开始播放媒体"
) )
} }
"pause" -> { "pause" -> {
// 尝试发送暂停广播 // 发送完整的按键事件序列
intent.action = "android.intent.action.MEDIA_PAUSE" val downEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PAUSE)
context.sendBroadcast(intent) val keyIntent = Intent(Intent.ACTION_MEDIA_BUTTON)
keyIntent.putExtra(Intent.EXTRA_KEY_EVENT, downEvent)
keyIntent.setPackage("com.yunqiinnovation.deep_voice")
context.sendBroadcast(keyIntent)
// 也尝试使用媒体按键广播 return Result(
val keyIntent = Intent(Intent.ACTION_MEDIA_BUTTON).apply {
putExtra(
Intent.EXTRA_KEY_EVENT,
android.view.KeyEvent(android.view.KeyEvent.ACTION_DOWN, android.view.KeyEvent.KEYCODE_MEDIA_PAUSE)
)
flags = Intent.FLAG_RECEIVER_FOREGROUND
}
context.sendOrderedBroadcast(keyIntent, null)
return Result(
success = true, success = true,
message = "已暂停媒体播放" message = "已暂停媒体播放"
) )
@ -468,7 +463,8 @@ class SystemFunctionHandler(private val context: Context) {
) )
flags = Intent.FLAG_RECEIVER_FOREGROUND flags = Intent.FLAG_RECEIVER_FOREGROUND
} }
context.sendOrderedBroadcast(keyIntent, null) keyIntent.setPackage("com.yunqiinnovation.deep_voice")
context.sendBroadcast(keyIntent)
return Result( return Result(
success = true, success = true,
message = "已切换到上一首" message = "已切换到上一首"
@ -487,7 +483,8 @@ class SystemFunctionHandler(private val context: Context) {
) )
flags = Intent.FLAG_RECEIVER_FOREGROUND flags = Intent.FLAG_RECEIVER_FOREGROUND
} }
context.sendOrderedBroadcast(keyIntent, null) keyIntent.setPackage("com.yunqiinnovation.deep_voice")
context.sendBroadcast(keyIntent)
return Result( return Result(
success = true, success = true,
message = "已切换到下一首" message = "已切换到下一首"

2
pubspec.yaml

@ -34,7 +34,7 @@ dependencies:
webview_flutter: ^4.10.0 webview_flutter: ^4.10.0
flutter_easyloading: ^3.0.5 flutter_easyloading: ^3.0.5
package_info_plus: ^8.3.0 package_info_plus: ^8.3.0
file_picker: ^8.0.0+1 file_picker: ^10.1.5
haptic_feedback: ^0.5.1+1 haptic_feedback: ^0.5.1+1
tencentcloud_cos_sdk_plugin: ^1.2.3 tencentcloud_cos_sdk_plugin: ^1.2.3

Loading…
Cancel
Save