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 (
id INTEGER PRIMARY KEY,
title TEXT,
content TEXT,
type TEXT,
seconds INTEGER,
address TEXT,
longitude TEXT,
latitude TEXT,
filepath TEXT,
audiourl TEXT,
creationtime INTEGER
)
''');
@ -43,9 +42,24 @@ class DatabaseHelper {
CREATE TABLE IF NOT EXISTS meetingdetails (
id INTEGER PRIMARY KEY,
meetingid INTEGER,
audio TEXT,
address TEXT,
personnel TEXT,
taskid TEXT,
tasktype INTEGER,
overview TEXT,
summary TEXT,
mindmap TEXT
)
''');
// 商务会议助手转写内容
batch.execute('''
CREATE TABLE IF NOT EXISTS meetingspeaker (
id INTEGER PRIMARY KEY,
meetingid INTEGER,
content TEXT,
aisummary TEXT
starttime INTEGER,
endtime INTEGER,
speaker TEXT
)
''');
await batch.commit();

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

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:chat_storage/chat_storage.dart';
import 'package:image_picker/image_picker.dart';
import 'package:get_storage/get_storage.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:app_settings/app_settings.dart';
class Message {
final bool isUser;
@ -51,6 +54,8 @@ class AgentController extends GetxController {
final isListening = false.obs;
final isSpeaking = false.obs;
final isProcessing = false.obs;
// TTS启用状态
final isTtsEnabled = true.obs;
// 图片处理状态
final isImageProcessing = false.obs;
@ -81,6 +86,7 @@ class AgentController extends GetxController {
@override
void onInit() {
super.onInit();
_loadTtsPreference();
_loadChatHistory();
_subscribeToEvents();
}
@ -95,6 +101,36 @@ class AgentController extends GetxController {
super.onClose();
}
// 加载TTS偏好设置
Future<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 {
try {
@ -134,12 +170,8 @@ class AgentController extends GetxController {
_addWelcomeMessage();
} finally {
isHistoryLoaded.value = true;
messages.refresh();
// 在加载完成后直接定位到底部,无需动画
WidgetsBinding.instance.addPostFrameCallback((_) {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
});
// 使用reverse:true时不需要手动滚动到底部
}
}
@ -199,7 +231,7 @@ class AgentController extends GetxController {
));
}
messages.refresh();
_scrollToBottom();
// 使用reverse:true时通常不需要手动滚动
}
break;
@ -229,8 +261,7 @@ class AgentController extends GetxController {
isProcessing.value = true;
// 标记为新的AI回复
_isNewAssistantResponse = true;
// 滚动到底部
_scrollToBottom();
// 使用reverse:true时通常不需要手动滚动
} else if (index >= 0) {
// 如果识别结果为空,移除临时的识别消息
messages.removeAt(index);
@ -282,8 +313,7 @@ class AgentController extends GetxController {
);
}
}
// 滚动到底部
_scrollToBottom();
// 使用reverse:true时通常不需要手动滚动
}
break;
@ -334,8 +364,7 @@ class AgentController extends GetxController {
// 发送此消息到AI处理
isProcessing.value = true;
_isNewAssistantResponse = true;
AgentService.processTextInput(messages[index].text,
speakResponse: true);
_handleAutoStop(index);
}
}
break;
@ -351,33 +380,35 @@ class AgentController extends GetxController {
final result = event.data['result'] ?? '';
final functionCall = event.data['function_call'] ?? '';
logger.i('Flutter 结束调用 mcp: $result, $functionCall');
if (result.isNotEmpty) {
final parsedResult = jsonDecode(result);
final meta = parsedResult["meta"] ?? "";
final meta = parsedResult["meta"] as String;
final metaResult = jsonDecode(meta);
logger.i('Flutter 结束调用 mcp metaResult: $metaResult');
// 判断是否为新的回复或响应ID是否改变
if (_isNewAssistantResponse || _currentResponseId != responseId) {
// 添加新消息
messages.add(Message(isUser: false, text: "", meta: metaResult));
_isNewAssistantResponse = false;
_currentResponseId = responseId;
} else {
// 更新最后一条消息
final lastIndex = messages.length - 1;
if (lastIndex >= 0 && !messages[lastIndex].isUser) {
messages[lastIndex] = Message(
isUser: false,
text: messages[lastIndex].text,
meta: meta,
timestamp: messages[lastIndex].timestamp,
);
if (meta.isNotEmpty) {
logger.i('Flutter 结束调用 mcp metaResult: $metaResult');
// 判断是否为新的回复或响应ID是否改变
if (_isNewAssistantResponse || _currentResponseId != responseId) {
// 添加新消息
messages
.add(Message(isUser: false, text: "", meta: metaResult));
_isNewAssistantResponse = false;
_currentResponseId = responseId;
} else {
// 更新最后一条消息
final lastIndex = messages.length - 1;
if (lastIndex >= 0 && !messages[lastIndex].isUser) {
messages[lastIndex] = Message(
isUser: false,
text: messages[lastIndex].text,
meta: metaResult,
timestamp: messages[lastIndex].timestamp,
);
}
}
messages.refresh();
// 滚动到底部
_scrollToBottom();
}
// 滚动到底部
_scrollToBottom();
}
break;
default:
@ -388,11 +419,13 @@ class AgentController extends GetxController {
// 滚动到底部
void _scrollToBottom() {
// 使用reverse:true时,此方法可大部分场景下不再需要
// 保留此方法但简化实现,仅用于特殊情况下的手动滚动
WidgetsBinding.instance.addPostFrameCallback((_) {
if (scrollController.hasClients) {
try {
scrollController.animateTo(
scrollController.position.maxScrollExtent,
0, // 使用reverse:true时,0表示列表底部
duration: Duration(milliseconds: 300),
curve: Curves.easeOut,
);
@ -423,14 +456,15 @@ class AgentController extends GetxController {
messages.add(message);
textController.clear();
// 滚动到底部
_scrollToBottom();
// 使用reverse:true时通常不需要手动滚动
try {
isProcessing.value = true;
// 标记为新的AI回复
_isNewAssistantResponse = true;
await AgentService.processTextInput(text, speakResponse: true);
// 根据TTS状态决定是否朗读
await AgentService.processTextInput(text,
speakResponse: isTtsEnabled.value);
} catch (e) {
logger.e('发送文本消息失败: $e');
isProcessing.value = false;
@ -459,8 +493,7 @@ class AgentController extends GetxController {
// 清空文本输入
textController.clear();
// 滚动到底部
_scrollToBottom();
// 使用reverse:true时通常不需要手动滚动
try {
isProcessing.value = true;
@ -468,11 +501,11 @@ class AgentController extends GetxController {
// 标记为新的AI回复
_isNewAssistantResponse = true;
// 调用Agent Service处理图片
// 调用Agent Service处理图片,考虑TTS状态
await AgentService.processImageInput(
imagePath,
text: text ?? '',
speakResponse: true,
speakResponse: isTtsEnabled.value,
);
} catch (e) {
logger.e('处理图片失败: $e');
@ -481,10 +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 {
if (isListening.value) return;
// 检查麦克风权限
final hasPermission = await _checkMicrophonePermission();
if (!hasPermission) {
isTextInputMode.value = true; // 如果没有权限,切回文本模式
return;
}
try {
await AgentService.startConversation();
} 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 {
try {
@ -645,4 +756,13 @@ class AgentController extends GetxController {
_musiceManager.resume();
isPlaying.value = true;
}
// 修改自动停止处理,遵循TTS设置
void _handleAutoStop(int index) {
// 发送此消息到AI处理
isProcessing.value = true;
_isNewAssistantResponse = true;
AgentService.processTextInput(messages[index].text,
speakResponse: isTtsEnabled.value);
}
}

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

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

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

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

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_easyloading/flutter_easyloading.dart';
import 'package:get/get.dart';
import 'package:just_audio/just_audio.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import '../../../core/utils/upload_oss.dart';
import '../../../data/services/db/sqflite_api.dart';
class MeetingController extends GetxController {
final ScrollController listController = ScrollController();
final AudioPlayer _audioPlayer = AudioPlayer();
// 是否还有更多数据
bool isLoadingMore = true;
@ -18,6 +27,7 @@ class MeetingController extends GetxController {
void onInit() {
super.onInit();
listController.addListener(_onScroll);
Permission.storage.request();
_getDataList();
}
@ -25,6 +35,7 @@ class MeetingController extends GetxController {
void onClose() {
listController.dispose();
titleController.dispose();
_audioPlayer.dispose();
super.onClose();
}
@ -48,23 +59,75 @@ class MeetingController extends GetxController {
}
}
void addAudio() async {
EasyLoading.show();
FilePickerResult? result = await FilePicker.platform.pickFiles(
withData: true,
);
if (result != null) {
PlatformFile file = result.files.first;
String filePath = await _saveFile(file);
String fileName = file.name;
await _audioPlayer.setAudioSource(AudioSource.file(filePath));
var audiourl =
await UploadOss.upload(filepath: filePath, rootDir: 'audio');
int seconds = _audioPlayer.duration?.inSeconds ?? 0;
int creationTime = DateTime.now().millisecondsSinceEpoch;
Map<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 {
if (title.isEmpty) return;
SqfliteApi.editMeetingTitle(id, title);
int index = dataList.indexWhere((item) => item['id'] == id);
if (index != -1) {
final dataMap = Map<String, dynamic>.from(dataList[index]);
dataMap['title'] = title;
dataList[index] = dataMap;
if (title.isNotEmpty) {
SqfliteApi.editMeetingTitle(id, title);
int index = dataList.indexWhere((item) => item['id'] == id);
if (index != -1) {
final dataMap = Map<String, dynamic>.from(dataList[index]);
dataMap['title'] = title;
dataList[index] = dataMap;
}
}
Get.back();
}
// 删除数据
void deleteMeeting(int id) {
void deleteMeeting(int id, String filepath) async {
dataList.removeWhere((item) => item['id'] == id);
// 删除数据库中的记录
SqfliteApi.deleteMeeting(id);
_deleteFile(filepath);
}
// 删除文件
Future<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_easyloading/flutter_easyloading.dart';
import 'package:get/get.dart';
import 'package:just_audio/just_audio.dart';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart' as http;
import 'package:uuid/uuid.dart';
import '../../../core/utils/logger.dart';
import '../../../core/utils/upload_oss.dart';
import '../../../data/models/appconfig_model.dart';
import '../../../data/services/ai_service.dart';
import '../../../data/services/db/sqflite_api.dart';
import '../../../data/services/open_ai_service_adapter.dart';
import '../model/meeting_details_model.dart';
import '../model/meeting_model.dart';
class MeetingDetailsController extends GetxController
with GetSingleTickerProviderStateMixin {
late AnimationController lottieController;
final AudioPlayer audioPlayer = AudioPlayer();
late ScrollController scrollController;
late TabController tabController;
final RxInt currentIndex = 0.obs;
final PlayerController playerController = PlayerController();
RxBool isTop = false.obs;
RxInt durationMax = 0.obs;
int id = 0;
RxBool isLoading = true.obs;
Rx<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
void onInit() {
super.onInit();
lottieController = AnimationController(vsync: this);
scrollController = ScrollController(initialScrollOffset: 0.0);
scrollController.addListener(_onScroll);
tabController = TabController(length: 4, vsync: this);
tabController.addListener(() {
currentIndex.value = tabController.index;
});
final Map<String, dynamic> args = Get.arguments ?? {};
id = args['id'];
_headers = {
'Authorization': 'Bearer; $_token',
'Content-Type': 'application/json',
};
_getAgent();
_getDataDetails();
}
@override
void onClose() {
lottieController.dispose();
audioPlayer.dispose();
scrollController.dispose();
tabController.dispose();
playerController.dispose();
_aiResponseSubscription?.cancel();
_isDisposed = true;
super.onClose();
}
void _onScroll() async {
double pixels = scrollController.position.pixels;
double maxScrollExtent = scrollController.position.maxScrollExtent;
if (!isTop.value && pixels > 5) {
await scrollController.animateTo(
maxScrollExtent,
duration: const Duration(milliseconds: 100),
curve: Curves.easeOut,
);
if (pixels > maxScrollExtent - 2) {
isTop.value = true;
}
}
if (isTop.value && pixels < maxScrollExtent - 5) {
await scrollController.animateTo(
0,
duration: const Duration(milliseconds: 100),
curve: Curves.easeOut,
);
if (pixels < 2) {
isTop.value = false;
}
}
}
void _getAgent() async {
final GetStorage storage = GetStorage();
// final List<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 {
final data = await SqfliteApi.getMeetingDetails(id);
meetingData.value = MeetingModel.fromJson(data);
dataDetails.value = MeetingDetailsModel.fromJson(data['details']);
audioPlayer.setAudioSource(AudioSource.file(dataDetails.value.audio));
audioPlayer.playerStateStream.listen((state) async {
if (state.processingState == ProcessingState.completed) {
await audioPlayer.seek(Duration.zero);
audioPlayer.pause();
lottieController.stop();
isPlay.value = false;
}
});
meetingDetails.value = MeetingDetailsModel.fromJson(data['details']);
await playerController.preparePlayer(
path: meetingData.value.filepath,
noOfSamples: 1200,
);
durationMax.value = await playerController.getDuration(DurationType.max);
playerController.setFinishMode(finishMode: FinishMode.pause);
if (meetingDetails.value.tasktype == 1) {
List speakerList = await SqfliteApi.getMeetingSpeaker(id);
textList.value = List.from(speakerList);
}
isLoading.value = false;
}
void start() async {
if (isPlay.value) {
lottieController.stop();
audioPlayer.pause();
isPlay.value = false;
void editSpeakerText() async {
if (editSpeakerTextId.value != 0 && editSpeakerTextChange.isNotEmpty) {
Map? foundElement = editSpeakerTextList.firstWhere(
(element) => element['id'] == editSpeakerTextId.value,
orElse: () => null,
);
if (foundElement != null) {
foundElement['content'] = editSpeakerTextChange;
} else {
editSpeakerTextList.add({
'id': editSpeakerTextId.value,
'content': editSpeakerTextChange,
});
}
}
if (editSpeakerTextList.isNotEmpty) {
EasyLoading.show();
var result = await SqfliteApi.editMeetingSpeaker(editSpeakerTextList);
if (result != null) {
for (var element in editSpeakerTextList) {
int index =
textList.indexWhere((item) => item['id'] == element['id']);
if (index != -1) {
final dataMap = Map<String, dynamic>.from(textList[index]);
dataMap['content'] = element['content'];
textList[index] = dataMap;
}
}
isSpeakerText.value = false;
editSpeakerTextId.value = 0;
editSpeakerTextList = [];
}
EasyLoading.dismiss();
} else {
lottieController.repeat();
audioPlayer.play();
isPlay.value = true;
isSpeakerText.value = false;
editSpeakerTextId.value = 0;
editSpeakerTextList = [];
}
}
void editSpeakerName() {}
void speakerCreate() async {
EasyLoading.show();
await _audioSpeaker();
EasyLoading.dismiss();
}
void summaryCreate() async {
EasyLoading.show();
if (meetingDetails.value.tasktype != 1) {
await _audioSpeaker();
}
if (allText.isEmpty) {
String text = '';
for (var element in textList) {
if (element['speaker'].isNotEmpty) {
text += '${element['speaker']}:';
}
text += '${element['content']}\n';
}
allText = text;
}
_aiServiceListen();
// const uuid = Uuid();
// _aiService.send(
// sid: uuid.v4(),
// messages: [
// {'role': 'user', 'content': allText},
// ],
// systemPrompt: _agent.systemPrompt,
// );
}
void _aiServiceListen() {
String summaryText = '';
// _aiResponseSubscription = _aiService.start().listen((AIStreamEvent event) {
// summaryText += event.content ?? '';
// if (event.type == StreamEventType.complete) {
// SqfliteApi.editMeeting(id, {'summary': summaryText});
// meetingDetails.value.summary = summaryText;
// meetingDetails.refresh();
// EasyLoading.dismiss();
// }
// });
}
Future<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 meetingid;
late String audio;
late String content;
late String aisummary;
late String address;
late String personnel;
late String taskid;
late int tasktype;
late String overview;
late String summary;
late String mindmap;
factory MeetingDetailsModel.fromJson(Map<String, dynamic> map) =>
MeetingDetailsModel()
..id = map["id"]
..meetingid = map["meetingid"]
..audio = map["audio"]
..content = map["content"]
..aisummary = map["aisummary"];
..address = map["address"]
..personnel = map["personnel"]
..taskid = map["taskid"]
..tasktype = map["tasktype"]
..overview = map["overview"]
..summary = map["summary"]
..mindmap = map["mindmap"];
Map<String, dynamic> toJson() {
final data = <String, dynamic>{};
data['id'] = id;
data['meetingid'] = meetingid;
data['audio'] = audio;
data['content'] = content;
data['aisummary'] = aisummary;
data['address'] = address;
data['personnel'] = personnel;
data['taskid'] = taskid;
data['tasktype'] = tasktype;
data['overview'] = overview;
data['summary'] = summary;
data['mindmap'] = mindmap;
return data;
}
}

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

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

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:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:lottie/lottie.dart';
import '../../../core/theme/app_colors.dart';
import '../../../routes/app_routes.dart';
import '../controllers/meeting_details_controller.dart';
import 'mind_map_tab.dart';
import 'overview_tab.dart';
import 'speech_tab.dart';
import 'summary_tab.dart';
class MeetingDetailsView extends GetView<MeetingDetailsController> {
const MeetingDetailsView({super.key});
@ -15,313 +18,356 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: _buildAppBar(isDarkMode),
body: Obx(() => !controller.isLoading.value
? _buildBody(isDarkMode)
: const SizedBox()),
return Obx(
() => Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: _buildAppBar(isDarkMode),
body: NestedScrollView(
controller: controller.scrollController,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
SliverAppBar(
automaticallyImplyLeading: false,
expandedHeight: 210.w,
backgroundColor: Colors.transparent,
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.pin,
background: Align(
alignment: Alignment.topCenter,
child: Container(
height: 200.w,
decoration: BoxDecoration(
color: Colors.grey[200]!,
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(20.r),
),
),
child: Column(
children: [
Container(
margin: EdgeInsets.only(bottom: 5.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8.r),
),
child: AudioFileWaveforms(
size: Size(350.w, 120.w),
playerController: controller.playerController,
playerWaveStyle: PlayerWaveStyle(
fixedWaveColor: Colors.grey[300]!,
liveWaveColor: Colors.red,
seekLineColor: Colors.red,
scaleFactor: 600,
),
),
),
SizedBox(
width: 350.w,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
StreamBuilder<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) {
return AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
backgroundColor:
controller.isTop.value ? Colors.grey[50] : Colors.grey[200],
surfaceTintColor: Colors.transparent,
elevation: 0.5,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
onPressed: () => Get.back(),
),
title: Text(
'recordingDetails'.tr, //录音详情
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
centerTitle: true,
);
}
// 构建主体内容
Widget _buildBody(bool isDarkMode) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
leading: controller.isSpeakerText.value
? GestureDetector(
onTap: () {
controller.isSpeakerText.value = false;
controller.editSpeakerTextId.value = 0;
controller.editSpeakerTextList = [];
},
child: Center(
child: Text(
controller.meetingData.value.title,
'cancel'.tr,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 17.sp,
fontSize: 16.sp,
),
),
),
10.horizontalSpace,
Text(
DateUtil.formatDateMs(
controller.meetingData.value.creationtime,
format: DateFormats.y_mo_d_h_m,
),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 14.sp,
),
)
: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
],
),
Padding(
padding: EdgeInsets.symmetric(vertical: 5.w),
child: Row(
children: [
Icon(
Icons.location_on,
size: 16.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
),
Expanded(
child: Text(
controller.meetingData.value.address,
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 14.sp,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
_audioWave(isDarkMode),
_recordContent(isDarkMode),
_bottomButton(isDarkMode),
],
),
);
}
// 音波
Widget _audioWave(bool isDarkMode) {
return Container(
padding: EdgeInsets.only(bottom: 10.w),
margin: EdgeInsets.symmetric(vertical: 5.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
SizedBox(
height: 70.w,
child: Row(
children: [
Expanded(
child: Lottie.asset(
'assets/lottie/audio_wave.json',
controller: controller.lottieController,
fit: BoxFit.fill,
onLoaded: (composition) {
controller.lottieController.duration =
composition.duration;
},
),
),
Expanded(
child: Lottie.asset(
'assets/lottie/audio_wave.json',
controller: controller.lottieController,
fit: BoxFit.fill,
),
),
Expanded(
child: Lottie.asset(
'assets/lottie/audio_wave.json',
controller: controller.lottieController,
fit: BoxFit.fill,
),
),
],
onPressed: () => Get.back(),
),
title: Opacity(
opacity: controller.isTop.value ? 1.0 : 0.0,
child: Container(
width: 170.w,
padding: EdgeInsets.symmetric(horizontal: 2.w, vertical: 3.w),
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(30.r),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
child: Row(
children: [
StreamBuilder<Duration>(
stream: controller.audioPlayer.positionStream,
StreamBuilder<PlayerState>(
stream: controller.playerController.onPlayerStateChanged,
builder: (context, snapshot) {
final position = snapshot.data ?? Duration.zero;
return Text(
DateUtil.formatDateMs(
position.inMilliseconds,
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 12.sp,
bool isPlay = snapshot.data?.isPlaying ?? false;
return GestureDetector(
onTap: () {
if (isPlay) {
controller.playerController.pausePlayer();
} else {
controller.playerController.startPlayer();
}
},
child: Icon(
isPlay ? Icons.pause_circle : Icons.play_circle,
color: Colors.black,
size: 30.w,
),
);
},
),
GestureDetector(
onTap: controller.start,
child: Container(
width: 30.w,
height: 30.w,
decoration: BoxDecoration(
color: isDarkMode ? AppColors.primary : Colors.blue,
borderRadius: BorderRadius.circular(
12.w,
),
),
child: Icon(
controller.isPlay.value ? Icons.pause : Icons.play_arrow,
color: Colors.white,
size: 25.w,
),
4.horizontalSpace,
AudioFileWaveforms(
size: Size(130.w, 30.w),
playerController: controller.playerController,
playerWaveStyle: PlayerWaveStyle(
fixedWaveColor: Colors.grey[400]!,
liveWaveColor: Colors.red,
seekLineColor: Colors.red,
scaleFactor: 100,
),
),
StreamBuilder<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) {
return Expanded(
child: Container(
width: double.infinity,
padding: EdgeInsets.all(10.w),
margin: EdgeInsets.symmetric(vertical: 5.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
// 构建主体内容
Widget _buildBody(BuildContext context, bool isDarkMode) {
return Column(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w),
margin: EdgeInsets.symmetric(vertical: 10.w),
child: Obx(
() => Row(
children: [
_iconItem(Icons.summarize, '概述', 0),
_iconItem(Icons.message, '转写', 1),
_iconItem(Icons.article, '总结', 2),
_iconItem(Icons.lan, '思维导图', 3),
],
),
],
),
),
child: DefaultTabController(
length: 2,
child: Column(
children: [
SizedBox(
height: 40.w,
child: TabBar(
indicatorSize: TabBarIndicatorSize.tab,
indicatorColor: isDarkMode ? AppColors.primary : Colors.blue,
labelColor: isDarkMode ? AppColors.primary : Colors.blue,
labelStyle: TextStyle(
color: isDarkMode
? Colors.white
: const Color.fromARGB(221, 197, 196, 196),
fontSize: 14.sp,
),
tabs: const [
Tab(text: 'AI总结'),
Tab(text: '会议原文'),
],
),
),
10.verticalSpace,
Expanded(
child: TabBarView(
children: [
SingleChildScrollView(
child: Text(
controller.dataDetails.value.aisummary,
),
),
SingleChildScrollView(
child: Text(
controller.dataDetails.value.content,
),
),
],
),
),
],
Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: TabBarView(
controller: controller.tabController,
physics: const NeverScrollableScrollPhysics(),
children: const [
OverviewTab(),
SpeechTab(),
SummaryTab(),
MindMapTab(),
],
),
),
),
),
],
);
}
// 底部按钮
Widget _bottomButton(bool isDarkMode) {
return Container(
padding: EdgeInsets.only(top: 20.w, bottom: 10.w),
alignment: Alignment.center,
Widget _iconItem(IconData icon, String title, int index) {
bool isActive = controller.currentIndex.value == index;
return Expanded(
flex: isActive ? 1 : 0,
child: GestureDetector(
onTap: () {
Get.toNamed(Routes.meetingEdit, arguments: {
'id': controller.id,
});
if (isActive) return;
controller.tabController.index = index;
controller.isSpeakerText.value = false;
controller.editSpeakerTextId.value = 0;
controller.editSpeakerTextList = [];
},
child: Container(
width: 130.w,
height: 48.w,
height: 45.w,
padding: EdgeInsets.symmetric(horizontal: 18.w),
margin: EdgeInsets.symmetric(horizontal: 4.w),
decoration: BoxDecoration(
color: isDarkMode ? AppColors.primary : Colors.blue,
color: isActive ? Colors.black : Colors.grey[200],
borderRadius: BorderRadius.circular(12.w),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.edit_calendar,
color: Colors.white,
size: 20.w,
),
5.horizontalSpace,
Text(
'编辑',
style: TextStyle(color: Colors.white, fontSize: 16.sp),
icon,
color: isActive ? Colors.white : Colors.grey[600],
size: 20.sp,
),
if (isActive)
Padding(
padding: EdgeInsets.only(left: 5.w),
child: Text(
title,
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
),
),
),
],
),
),

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_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:haptic_feedback/haptic_feedback.dart';
import '../../../core/theme/app_colors.dart';
import '../../../routes/app_routes.dart';
@ -19,6 +20,10 @@ class MeetingView extends GetView<MeetingController> {
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: _buildAppBar(isDarkMode),
body: _buildBody(context, isDarkMode),
floatingActionButton: FloatingActionButton(
onPressed: controller.addAudio,
child: const Icon(Icons.add),
),
);
}
@ -49,50 +54,15 @@ class MeetingView extends GetView<MeetingController> {
// 构建主体内容
Widget _buildBody(BuildContext context, bool isDarkMode) {
return Stack(
children: [
Obx(
() => ListView.builder(
itemCount: controller.dataList.length,
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w),
itemBuilder: (BuildContext context, int index) {
MeetingModel data =
MeetingModel.fromJson(controller.dataList[index]);
return _item(context, isDarkMode, data);
},
),
),
Align(
alignment: Alignment.bottomCenter,
child: GestureDetector(
onTap: () {
_bottomSheet(context, isDarkMode);
},
child: Container(
//width: 130.w,
height: 48.w,
margin: EdgeInsets.symmetric(vertical: 20.w),
padding: EdgeInsets.symmetric(horizontal: 14.w),
decoration: BoxDecoration(
color: isDarkMode ? AppColors.primary : Colors.blue,
borderRadius: BorderRadius.circular(12.w),
),
child: Row(
mainAxisSize: MainAxisSize.min, // 添加这行使Row宽度自适应内容
//mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.keyboard_voice, color: Colors.white),
5.horizontalSpace,
Text(
'startMeeting'.tr,// 开始会议
style: TextStyle(color: Colors.white, fontSize: 16.sp),
),
],
),
),
),
),
],
return Obx(
() => ListView.builder(
itemCount: controller.dataList.length,
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w),
itemBuilder: (BuildContext context, int index) {
MeetingModel data = MeetingModel.fromJson(controller.dataList[index]);
return _item(context, isDarkMode, data);
},
),
);
}
@ -103,52 +73,11 @@ class MeetingView extends GetView<MeetingController> {
'id': data.id,
});
},
onLongPressStart: (details) {
RenderBox? renderBox =
Overlay.of(context).context.findRenderObject() as RenderBox;
final RelativeRect position = RelativeRect.fromRect(
Rect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx + 110, // 菜单显示位置X轴坐标
details.globalPosition.dy - 40, // 菜单显示位置Y轴坐标
),
Offset.zero & renderBox.size,
);
showMenu(
context: context,
position: position,
color: isDarkMode ? AppColors.cardBackground : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
items: [
_popupMenuEntry(
Icon(
Icons.edit_calendar,
color: isDarkMode ? Colors.white : Colors.black87,
),
'edit'.tr,// 编辑
isDarkMode ? Colors.white : Colors.black87,
() {
controller.titleController.text = data.title;
_bottomSheet(context, isDarkMode, id: data.id);
},
),
_popupMenuEntry(
const Icon(Icons.delete_outline, color: Colors.red),
'delete'.tr,// 删除
isDarkMode ? Colors.white : Colors.black87,
() {
controller.deleteMeeting(data.id);
},
),
],
);
onLongPressStart: (LongPressStartDetails details) {
_onLongPressItem(context, isDarkMode, details, data);
},
child: Container(
padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 8.w),
padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w),
margin: EdgeInsets.symmetric(vertical: 5.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
@ -164,89 +93,67 @@ class MeetingView extends GetView<MeetingController> {
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
data.title,
Text(
data.title,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 18.sp,
),
overflow: TextOverflow.ellipsis,
),
Padding(
padding: EdgeInsets.symmetric(vertical: 5.w),
child: Row(
children: [
Icon(
Icons.calendar_month,
size: 14.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
),
2.horizontalSpace,
Text(
DateUtil.formatDateMs(
data.creationtime,
format: DateFormats.y_mo_d_h_m,
),
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 16.sp,
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 12.sp,
),
overflow: TextOverflow.ellipsis,
),
),
10.horizontalSpace,
Text(
DateUtil.formatDateMs(
data.creationtime,
format: DateFormats.y_mo_d_h_m,
20.horizontalSpace,
Icon(
Icons.access_time,
size: 14.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
),
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
fontSize: 12.sp,
),
),
],
),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
data.content.isNotEmpty ? data.content : 'noContent'.tr,// 无内容
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
fontSize: 12.sp,
),
overflow: TextOverflow.ellipsis,
),
Row(
children: [
Icon(
Icons.location_on,
size: 14.w,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
Expanded(
child: Text(
data.address,
style: TextStyle(
color: isDarkMode
? Colors.white70
: Colors.black54,
fontSize: 12.sp,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
Container(
width: 76.w,
height: 28.w,
margin: EdgeInsets.only(left: 10.w),
alignment: Alignment.center,
decoration: BoxDecoration(
color: isDarkMode ? AppColors.primary : Colors.blue,
borderRadius: BorderRadius.circular(28.w),
),
child: Text(
DateUtil.formatDateMs(
data.seconds * 1000,
format: 'HH:mm:ss',
isUtc: true,
2.horizontalSpace,
Text(
'${data.seconds}s',
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 12.sp,
),
style: TextStyle(color: Colors.white, fontSize: 14.sp),
),
],
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 2.w),
margin: EdgeInsets.only(top: 5.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.grey[100],
borderRadius: BorderRadius.circular(4.w),
),
child: Text(
data.type,
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
fontSize: 10.sp,
),
],
),
),
],
),
@ -254,6 +161,58 @@ class MeetingView extends GetView<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(
Icon icon,
String title,
@ -263,14 +222,17 @@ class MeetingView extends GetView<MeetingController> {
return PopupMenuItem(
onTap: onTap,
child: SizedBox(
width: 120.w,
width: 140.w,
child: Row(
children: [
icon,
8.horizontalSpace,
Text(
title,
style: TextStyle(color: color),
style: TextStyle(
color: color,
fontSize: 14.sp,
),
),
],
),
@ -280,9 +242,9 @@ class MeetingView extends GetView<MeetingController> {
void _bottomSheet(
BuildContext context,
bool isDarkMode, {
int? id,
}) {
bool isDarkMode,
int id,
) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
@ -298,7 +260,7 @@ class MeetingView extends GetView<MeetingController> {
mainAxisSize: MainAxisSize.min,
children: [
Text(
id != null ? 'editMeetingTitle'.tr : 'meetingTitle'.tr, // 编辑会议标题
'editMeetingTitle'.tr, // 编辑会议标题
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 16.sp,
@ -363,7 +325,7 @@ class MeetingView extends GetView<MeetingController> {
borderRadius: BorderRadius.circular(12.w),
),
child: Text(
'cancel'.tr,// 取消
'cancel'.tr, // 取消
style: TextStyle(
fontSize: 14.sp,
color:
@ -374,20 +336,8 @@ class MeetingView extends GetView<MeetingController> {
),
GestureDetector(
onTap: () {
int time = DateTime.now().millisecondsSinceEpoch;
String title = controller.titleController.text;
if (id != null) {
controller.editMeetingTitle(id, title);
} else {
if (title.isEmpty) {
title =
'${DateUtil.formatDateMs(time, format: DateFormats.y_mo_d)} ${'meeting'.tr}';
}
Get.back();
Get.toNamed(Routes.meetingRecord, arguments: {
'title': title,
});
}
controller.editMeetingTitle(id, title);
},
child: Container(
width: 120.w,
@ -398,7 +348,7 @@ class MeetingView extends GetView<MeetingController> {
borderRadius: BorderRadius.circular(12.w),
),
child: Text(
'confirm'.tr,// 确认
'confirm'.tr, // 确认
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
@ -414,8 +364,6 @@ class MeetingView extends GetView<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) {
permissionsToRequest.add(Permission.locationWhenInUse);
permissionsToRequest.add(Permission.microphone);
// permissionsToRequest.add(Permission.microphone);
if (await _getAndroidSdkVersion() >= 31) {
permissionsToRequest.add(Permission.bluetoothScan);
permissionsToRequest.add(Permission.bluetoothConnect);
@ -235,6 +235,9 @@ class PairingController extends GetxController {
}
_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/meeting/bindings/meeting_binding.dart';
import '../modules/meeting/bindings/meeting_details_binding.dart';
import '../modules/meeting/bindings/meeting_edit_binding.dart';
import '../modules/meeting/bindings/meeting_record_binding.dart';
import '../modules/meeting/views/meeting_details_view.dart';
import '../modules/meeting/views/meeting_edit_view.dart';
import '../modules/meeting/views/meeting_record_view.dart';
import '../modules/meeting/views/meeting_view.dart';
import '../modules/profile/views/profile_view.dart';
import '../modules/profile/bindings/profile_binding.dart';
@ -138,13 +134,6 @@ abstract class AppPages {
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.meetingRecord,
page: () => const MeetingRecordView(),
binding: MeetingRecordBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.meetingDetails,
page: () => const MeetingDetailsView(),
@ -152,13 +141,6 @@ abstract class AppPages {
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.meetingEdit,
page: () => const MeetingEditView(),
binding: MeetingEditBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.timbre,
page: () => const TimbreView(),

4
lib/routes/app_routes.dart

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

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) {
audioPlayer?.stopAudio()
FileLogger.d(TAG, "mcp调用结果: $functionCallResult")
sendEvent("function_call_result", mapOf(
"function_call" to functionCall.toString(),
"result" to functionCallResult.toString(),
))
FileLogger.d(TAG, "mcp调用结果: $functionCallResult")
FileLogger.d(TAG, "mcp调用结果: $functionCallResult")
metadata = autoPlayMusic(functionCallResult);
}
}

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
// 回音消除
echoCanceler = AcousticEchoCanceler.create(sessionId).apply {
enabled = true
}
echoCanceler = AcousticEchoCanceler.create(sessionId)
echoCanceler?.enabled = true;
}
/**

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

@ -7,20 +7,19 @@
<application>
<service
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>
<action android:name="android.intent.action.MEDIA_BUTTON" />
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<receiver
android:name="androidx.media.session.MediaButtonReceiver"
android:exported="false">
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</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 {
FileLogger.i(TAG, "onStartCommand: $intent")
intent?.action?.takeIf { it == Intent.ACTION_MEDIA_BUTTON }?.let {
// 分发给 MediaSession 处理媒体按钮
MediaButtonReceiver.handleIntent(mediaSession, intent)
@ -62,7 +63,6 @@ class MusicService : MediaBrowserServiceCompat() {
}
val command = intent?.getStringExtra("command") ?: return START_NOT_STICKY
FileLogger.i(TAG, "liwei------------------- onStartCommand: $command")
when (command) {
"play" -> intent.getSerializableExtra("song")?.let { url ->
val songMap = intent.getSerializableExtra("song") as? Map<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("com.google.code.gson:gson:2.10.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
implementation("androidx.media:media:1.6.0")
}

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

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

2
pubspec.yaml

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

Loading…
Cancel
Save