From d9c3c56597d08aa4c9eff4b1d8b4c2e63b928251 Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Mon, 31 Mar 2025 11:05:12 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/modules/login/views/login_view.dart | 10 +- lib/modules/settings/views/settings_view.dart | 136 ++++++++++++------ 2 files changed, 97 insertions(+), 49 deletions(-) diff --git a/lib/modules/login/views/login_view.dart b/lib/modules/login/views/login_view.dart index 0a4146393..ad2597f65 100644 --- a/lib/modules/login/views/login_view.dart +++ b/lib/modules/login/views/login_view.dart @@ -171,7 +171,7 @@ class LoginView extends GetView { ); } - // 输入框 + // 输入框 Widget _buildInputField(bool isDarkMode) { return Container( decoration: BoxDecoration( @@ -224,13 +224,13 @@ class LoginView extends GetView { Expanded( child: TextField( controller: controller.contactController, - // keyboardType: controller.selectedCountryFlag == '✉️' + // keyboardType: controller.selectedCountryFlag == '✉️' // ? TextInputType.emailAddress // : TextInputType.phone, decoration: InputDecoration( - hintText: controller.selectedCountryFlag == '✉️' - ? 'email'.tr // 需要在翻译文件中添加 - : 'phoneNumber'.tr, // 需要在翻译文件中添加 + hintText: controller.selectedCountryFlag == '✉️' + ? 'email'.tr // 需要在翻译文件中添加 + : 'phoneNumber'.tr, // 需要在翻译文件中添加 hintStyle: TextStyle( fontSize: 14.sp, color: isDarkMode ? Colors.grey[600] : Colors.black38, diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index 564cf4443..ce4f7866a 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -45,6 +45,8 @@ class SettingsView extends GetView { children: [ _buildUserInfoCard(isDarkMode), SizedBox(height: 20.h), + _buildAISettingsSection(isDarkMode), + SizedBox(height: 20.h), _buildSystemSettingsSection(isDarkMode), SizedBox(height: 20.h), _buildAboutSection(isDarkMode), @@ -110,12 +112,12 @@ class SettingsView extends GetView { // 裁剪圆形头像 borderRadius: BorderRadius.circular(30.r), child: controller.userInfo.value.avatar.isEmpty - ? - // Image.network( - // 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?ixlib=rb-1.2.1&auto=format&fit=crop&w=200&q=80', - // fit: BoxFit.cover, // 图片填充方式 - // ) - Container( + ? + // Image.network( + // 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?ixlib=rb-1.2.1&auto=format&fit=crop&w=200&q=80', + // fit: BoxFit.cover, // 图片填充方式 + // ) + Container( decoration: BoxDecoration( color: Colors.blue[600], borderRadius: BorderRadius.circular(30.r), @@ -142,12 +144,15 @@ class SettingsView extends GetView { return Container( decoration: BoxDecoration( color: Colors.blue[600], - borderRadius: BorderRadius.circular(30.r), + borderRadius: + BorderRadius.circular(30.r), ), child: Center( child: Text( - controller.userInfo.value.name.isNotEmpty - ? controller.userInfo.value.name[0] + controller.userInfo.value.name + .isNotEmpty + ? controller + .userInfo.value.name[0] .toUpperCase() : '?', style: TextStyle( @@ -266,6 +271,82 @@ class SettingsView extends GetView { ); } + // AI设置 + Widget _buildAISettingsSection(bool isDarkMode) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(left: 4.w, bottom: 8.h), + child: Text( + 'AI设置', + style: TextStyle( + fontSize: 12.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.blue[400] : Colors.blue[600], + letterSpacing: 1, + ), + ), + ), + Container( + decoration: BoxDecoration( + color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white, + borderRadius: BorderRadius.circular(16.r), + boxShadow: [ + BoxShadow( + color: isDarkMode + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16.r), + child: BackdropFilter( + filter: isDarkMode + ? ImageFilter.blur(sigmaX: 5, sigmaY: 5) + : ImageFilter.blur(sigmaX: 0, sigmaY: 0), + child: Column( + children: [ + // 语音服务选择 + _buildSpeechServiceSetting(isDarkMode), + Divider( + height: 1, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[200]), + Obx( + () => _buildNavigationSetting( + title: 'timbreService'.tr, // 音色服务 + subtitle: 'timbreServiceInfo'.tr, // 选择音色服务 + icon: Icons.photo_camera_front_outlined, + iconBgColor: isDarkMode + ? Colors.cyan[900]!.withOpacity(0.3) + : Colors.cyan[100]!, + iconColor: + isDarkMode ? Colors.cyan[300]! : Colors.cyan[600]!, + value: controller.timbreName.value.tr, + valueStyle: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.white70 : Colors.grey[500], + ), + onTap: () { + Get.toNamed(Routes.timbre); + }, + isDarkMode: isDarkMode, + ), + ), + ], + ), + ), + ), + ), + ], + ); + } + // 系统设置部分 Widget _buildSystemSettingsSection(bool isDarkMode) { return Column( @@ -370,46 +451,13 @@ class SettingsView extends GetView { ), onTap: () { // 打开备份设置页面 - print('---------用户头像-${controller.userInfo.value.avatar}'); + print( + '---------用户头像-${controller.userInfo.value.avatar}'); print('---------用户名-${controller.userInfo.value.name}'); //print('user${User.instance.value}'); }, isDarkMode: isDarkMode, ), - Divider( - height: 1, - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.grey[200]), - - // 语音服务选择 - _buildSpeechServiceSetting(isDarkMode), - Divider( - height: 1, - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.grey[200]), - Obx( - () => _buildNavigationSetting( - title: 'timbreService'.tr, // 音色服务 - subtitle: 'timbreServiceInfo'.tr, // 选择音色服务 - icon: Icons.photo_camera_front_outlined, - iconBgColor: isDarkMode - ? Colors.cyan[900]!.withOpacity(0.3) - : Colors.cyan[100]!, - iconColor: - isDarkMode ? Colors.cyan[300]! : Colors.cyan[600]!, - value: controller.timbreName.value.tr, - valueStyle: TextStyle( - fontSize: 12.sp, - color: isDarkMode ? Colors.white70 : Colors.grey[500], - ), - onTap: () { - Get.toNamed(Routes.timbre); - }, - isDarkMode: isDarkMode, - ), - ), Divider( height: 1, color: isDarkMode From 854118c83d0b897be4e73fd88eb54eb35e9ab1ce Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Mon, 31 Mar 2025 15:07:05 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BC=9A=E8=AE=AE?= =?UTF-8?q?=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/services/db/database_helper.dart | 2 +- .../meeting_details_controller.dart | 3 +- .../controllers/meeting_edit_controller.dart | 4 +- .../meeting_record_controller.dart | 38 ++- .../meeting/model/meeting_details_model.dart | 4 +- .../meeting/views/meeting_record_view.dart | 246 ++++++++++-------- lib/modules/meeting/views/meeting_view.dart | 6 +- 7 files changed, 177 insertions(+), 126 deletions(-) diff --git a/lib/data/services/db/database_helper.dart b/lib/data/services/db/database_helper.dart index f8914fa37..319935941 100644 --- a/lib/data/services/db/database_helper.dart +++ b/lib/data/services/db/database_helper.dart @@ -40,7 +40,7 @@ class DatabaseHelper { CREATE TABLE IF NOT EXISTS meetingdetails ( id INTEGER PRIMARY KEY, meetingid INTEGER, - audio BLOB, + audio TEXT, content TEXT ) '''); diff --git a/lib/modules/meeting/controllers/meeting_details_controller.dart b/lib/modules/meeting/controllers/meeting_details_controller.dart index 37c3b3259..c1319036d 100644 --- a/lib/modules/meeting/controllers/meeting_details_controller.dart +++ b/lib/modules/meeting/controllers/meeting_details_controller.dart @@ -3,7 +3,6 @@ import 'package:get/get.dart'; import 'package:just_audio/just_audio.dart'; import '../../../data/services/db/sqflite_api.dart'; -import '../../../data/services/speech_impl/volcano_tts_api_service.dart'; import '../model/meeting_details_model.dart'; import '../model/meeting_model.dart'; @@ -42,7 +41,7 @@ class MeetingDetailsController extends GetxController final data = await SqfliteApi.getMeetingDetails(id); meetingData.value = MeetingModel.fromJson(data); dataDetails.value = MeetingDetailsModel.fromJson(data['details']); - audioPlayer.setAudioSource(BytesAudioSource(dataDetails.value.audio)); + audioPlayer.setAudioSource(AudioSource.file(dataDetails.value.audio)); audioPlayer.playerStateStream.listen((state) async { if (state.processingState == ProcessingState.completed) { await audioPlayer.seek(Duration.zero); diff --git a/lib/modules/meeting/controllers/meeting_edit_controller.dart b/lib/modules/meeting/controllers/meeting_edit_controller.dart index 5ae01a749..9b5702d80 100644 --- a/lib/modules/meeting/controllers/meeting_edit_controller.dart +++ b/lib/modules/meeting/controllers/meeting_edit_controller.dart @@ -3,7 +3,6 @@ import 'package:get/get.dart'; import 'package:just_audio/just_audio.dart'; import '../../../data/services/db/sqflite_api.dart'; -import '../../../data/services/speech_impl/volcano_tts_api_service.dart'; import 'meeting_controller.dart'; import 'meeting_details_controller.dart'; @@ -53,7 +52,7 @@ class MeetingEditController extends GetxController ); creationTime = meetingDetailsController.meetingData.value.creationtime; audioPlayer.setAudioSource( - BytesAudioSource(meetingDetailsController.dataDetails.value.audio)); + AudioSource.file(meetingDetailsController.dataDetails.value.audio)); audioPlayer.playerStateStream.listen((state) async { if (state.processingState == ProcessingState.completed) { await audioPlayer.seek(Duration.zero); @@ -71,7 +70,6 @@ class MeetingEditController extends GetxController isPlay.value = false; } else { lottieController.repeat(); - await audioPlayer.seek(Duration.zero); audioPlayer.play(); isPlay.value = true; } diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index 17c1849fa..25f0d4c5f 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -1,9 +1,13 @@ import 'dart:async'; +import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.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/asr_service.dart'; import '../../../data/services/db/sqflite_api.dart'; import '../../../data/services/speech_impl/xunfei_asr_service.dart'; @@ -34,6 +38,7 @@ class MeetingRecordController extends GetxController @override void onInit() { super.onInit(); + Permission.storage.request(); lottieController = AnimationController(vsync: this); Get.put(XunfeiAsrService()); _asrService = Get.find(); @@ -144,6 +149,20 @@ class MeetingRecordController extends GetxController // 完成 void complete() async { + if (await Permission.storage.request().isGranted) { + _save(); + } else { + Get.snackbar( + 'error'.tr, // 错误 + '请开启文件存储权限', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.1), + colorText: Colors.red, + ); + } + } + + void _save() async { String firstContent = finalContent.value.length >= 30 ? finalContent.value.substring(0, 30) : finalContent.value; @@ -155,12 +174,15 @@ class MeetingRecordController extends GetxController dataLength: audio.length, ); Uint8List wavData = Uint8List.fromList([...wavHeader, ...audio]); + String audiopPath = + await _saveAudioToFile(wavData, 'meeting_$creationTime.wav'); + if (audiopPath.isEmpty) return; Map data = { 'title': title, 'firstContent': firstContent.replaceAll("\n", ""), 'seconds': seconds.value, 'creationTime': creationTime, - 'audio': wavData, + 'audio': audiopPath, 'content': finalContent.value, }; final result = await SqfliteApi.insertMeeting(data); @@ -176,6 +198,20 @@ class MeetingRecordController extends GetxController } } + Future _saveAudioToFile(Uint8List audioData, String fileName) async { + try { + final directory = await getApplicationDocumentsDirectory(); + final file = File('${directory.path}/$fileName'); + + // 写入文件 + await file.writeAsBytes(audioData); + return file.path; + } catch (e) { + Logger.error('保存失败: $e'); + return ''; + } + } + Uint8List generateWavHeader({ required int sampleRate, required int numberOfChannels, diff --git a/lib/modules/meeting/model/meeting_details_model.dart b/lib/modules/meeting/model/meeting_details_model.dart index 0e8798640..2b2845eff 100644 --- a/lib/modules/meeting/model/meeting_details_model.dart +++ b/lib/modules/meeting/model/meeting_details_model.dart @@ -1,11 +1,9 @@ -import 'dart:typed_data'; - class MeetingDetailsModel { MeetingDetailsModel(); late int id; late int meetingid; - late Uint8List audio; + late String audio; late String content; factory MeetingDetailsModel.fromJson(Map map) => diff --git a/lib/modules/meeting/views/meeting_record_view.dart b/lib/modules/meeting/views/meeting_record_view.dart index 1079e699d..f3467ce11 100644 --- a/lib/modules/meeting/views/meeting_record_view.dart +++ b/lib/modules/meeting/views/meeting_record_view.dart @@ -90,86 +90,36 @@ class MeetingRecordView extends GetView { // 构建主体内容 Widget _buildBody(bool isDarkMode) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 10.verticalSpace, + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w), + child: Text( controller.title, style: TextStyle( color: isDarkMode ? Colors.white : Colors.black87, fontSize: 17.sp, ), ), - 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, - ), - ), - ), - _audioWave(isDarkMode), - _recordContent(isDarkMode), - _bottomButton(isDarkMode), - ], - ), - ); - } - - // 音波 - Widget _audioWave(bool isDarkMode) { - return Container( - width: double.infinity, - height: 70.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: 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, + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w), + child: Text( + DateUtil.formatDateMs( + controller.creationTime, + format: DateFormats.y_mo_d_h_m, ), - ), - Expanded( - child: Lottie.asset( - 'assets/lottie/audio_wave.json', - controller: controller.lottieController, - fit: BoxFit.fill, + style: TextStyle( + color: isDarkMode ? Colors.grey[400] : Colors.grey[500], + fontSize: 14.sp, ), ), - ], - ), + ), + _recordContent(isDarkMode), + _bottomButton(isDarkMode), + ], ); } @@ -179,7 +129,7 @@ class MeetingRecordView extends GetView { child: Container( width: double.infinity, padding: EdgeInsets.all(10.w), - margin: EdgeInsets.symmetric(vertical: 5.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), @@ -210,51 +160,121 @@ class MeetingRecordView extends GetView { // 底部按钮 Widget _bottomButton(bool isDarkMode) { - return Padding( - padding: EdgeInsets.only(top: 40.w, bottom: 30.w), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - onTap: controller.recorderHandle, - child: Container( - width: 130.w, - height: 60.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(60.w), - border: Border.all( - color: isDarkMode ? AppColors.primary : Colors.blue, - width: 2.w, + 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, + ), + ), + ), ), - ), - child: Obx( - () => Icon( - controller.isRecording.value ? Icons.pause : Icons.play_arrow, - color: isDarkMode ? AppColors.primary : Colors.blue, - size: 40.w, + 10.horizontalSpace, + Expanded( + child: Lottie.asset( + 'assets/lottie/audio_wave.json', + controller: controller.lottieController, + fit: BoxFit.fill, + onLoaded: (composition) { + controller.lottieController.duration = + composition.duration; + }, + ), ), - ), - ), - ), - GestureDetector( - onTap: () { - _showAlertDialog(isDarkMode); - }, - child: Container( - width: 130.w, - height: 60.w, - decoration: BoxDecoration( - color: isDarkMode ? AppColors.primary : Colors.blue, - borderRadius: BorderRadius.circular(60.w), - ), - child: Icon( - Icons.fiber_manual_record_outlined, - color: Colors.white, - size: 40.w, - ), + 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); + }, + ), + ), + ), + ], ), ), - ], + ), ), ); } diff --git a/lib/modules/meeting/views/meeting_view.dart b/lib/modules/meeting/views/meeting_view.dart index 10678dc88..8f05c498b 100644 --- a/lib/modules/meeting/views/meeting_view.dart +++ b/lib/modules/meeting/views/meeting_view.dart @@ -82,7 +82,7 @@ class MeetingView extends GetView { const Icon(Icons.keyboard_voice, color: Colors.white), 5.horizontalSpace, Text( - '录音', + '开始会议', style: TextStyle(color: Colors.white, fontSize: 16.sp), ), ], @@ -264,7 +264,7 @@ class MeetingView extends GetView { mainAxisSize: MainAxisSize.min, children: [ Text( - id != null ? '修改录音标题(非必填)' : '录音标题(非必填)', + id != null ? '修改会议标题(非必填)' : '会议标题(非必填)', style: TextStyle( color: isDarkMode ? Colors.white : Colors.black87, fontSize: 16.sp, @@ -347,7 +347,7 @@ class MeetingView extends GetView { } else { if (title.isEmpty) { title = - '${DateUtil.formatDateMs(time, format: DateFormats.y_mo_d)} 录音'; + '${DateUtil.formatDateMs(time, format: DateFormats.y_mo_d)} 会议'; } Get.back(); Get.toNamed(Routes.meetingRecord, arguments: {