From c15894fb605ed130874f9b5fdba1e398d2ed8e27 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Mon, 23 Jun 2025 16:57:22 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E9=9D=A2=E5=AF=B9=E9=9D=A2?= =?UTF-8?q?=E7=BF=BB=E8=AF=91=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?ui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../speech_impl/azure_tts_service.dart | 6 +- .../speech_impl/voice_clone_tts_service.dart | 2 +- lib/data/services/tts_service.dart | 4 +- lib/modules/explore/views/explore_view.dart | 62 +- lib/modules/meeting/views/meeting_view.dart | 1075 +++++++++-------- .../controllers/translation_controller.dart | 85 +- .../views/translation_list_view.dart | 20 +- .../translation/views/translation_view.dart | 8 +- .../azure_speech/AzureAsrHelper.kt | 13 +- .../azure_speech/AzureSpeechPlugin.kt | 14 +- .../azure_speech/AzureTtsHelper.kt | 1 + 11 files changed, 688 insertions(+), 602 deletions(-) diff --git a/lib/data/services/speech_impl/azure_tts_service.dart b/lib/data/services/speech_impl/azure_tts_service.dart index 8b2512260..56d90551f 100644 --- a/lib/data/services/speech_impl/azure_tts_service.dart +++ b/lib/data/services/speech_impl/azure_tts_service.dart @@ -415,10 +415,12 @@ class AzureTtsService extends GetxService implements TtsService { } @override - Future setAudioOutputDevice() async { + Future setAudioOutputDevice(int type) async { try { // 开始合成,传递参数 - final result = await _channel.invokeMethod('setAudioOutputDevice'); + final result = await _channel.invokeMethod('setAudioOutputDevice', { + 'type': type, + }); return result; } catch (e) { Logger.error('音频输出设备类型失败: ${e.toString()}'); diff --git a/lib/data/services/speech_impl/voice_clone_tts_service.dart b/lib/data/services/speech_impl/voice_clone_tts_service.dart index 656fe4bcb..4fa6514ed 100644 --- a/lib/data/services/speech_impl/voice_clone_tts_service.dart +++ b/lib/data/services/speech_impl/voice_clone_tts_service.dart @@ -392,7 +392,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService { } @override - Future setAudioOutputDevice() { + Future setAudioOutputDevice(int type) { // TODO: implement setAudioOutputDevice throw UnimplementedError(); } diff --git a/lib/data/services/tts_service.dart b/lib/data/services/tts_service.dart index 4e2ec3ef4..996437def 100644 --- a/lib/data/services/tts_service.dart +++ b/lib/data/services/tts_service.dart @@ -33,7 +33,9 @@ abstract class TtsService { /// 释放资源 Future dispose(); - Future setAudioOutputDevice(); + + /// 切换音频输出设备 + Future setAudioOutputDevice(int type); } /// TTS事件类型 diff --git a/lib/modules/explore/views/explore_view.dart b/lib/modules/explore/views/explore_view.dart index 0f076f6e9..e487c7fbb 100644 --- a/lib/modules/explore/views/explore_view.dart +++ b/lib/modules/explore/views/explore_view.dart @@ -126,37 +126,37 @@ class ExploreView extends GetView { ], ), - // 搜索框 - SizedBox(height: 16.h), - Container( - decoration: BoxDecoration( - color: - isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], - borderRadius: BorderRadius.circular(12.r), - ), - child: TextField( - controller: controller.searchController, - decoration: InputDecoration( - hintText: 'searchAi'.tr, // 搜索AI功能... - hintStyle: TextStyle( - fontSize: 14.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[400], - ), - prefixIcon: Icon( - Icons.search, - size: 20.sp, - color: isDarkMode ? Colors.grey[400] : Colors.grey[400], - ), - border: InputBorder.none, - contentPadding: EdgeInsets.symmetric(vertical: 12.h), - ), - textInputAction: TextInputAction.search, - onSubmitted: (_) => controller.search(), - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black, - ), - ), - ), + // // 搜索框 + // SizedBox(height: 16.h), + // Container( + // decoration: BoxDecoration( + // color: + // isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100], + // borderRadius: BorderRadius.circular(12.r), + // ), + // child: TextField( + // controller: controller.searchController, + // decoration: InputDecoration( + // hintText: 'searchAi'.tr, // 搜索AI功能... + // hintStyle: TextStyle( + // fontSize: 14.sp, + // color: isDarkMode ? Colors.grey[400] : Colors.grey[400], + // ), + // prefixIcon: Icon( + // Icons.search, + // size: 20.sp, + // color: isDarkMode ? Colors.grey[400] : Colors.grey[400], + // ), + // border: InputBorder.none, + // contentPadding: EdgeInsets.symmetric(vertical: 12.h), + // ), + // textInputAction: TextInputAction.search, + // onSubmitted: (_) => controller.search(), + // style: TextStyle( + // color: isDarkMode ? Colors.white : Colors.black, + // ), + // ), + // ), ], ), ); diff --git a/lib/modules/meeting/views/meeting_view.dart b/lib/modules/meeting/views/meeting_view.dart index 2a1c2c7e9..b4322b9b5 100644 --- a/lib/modules/meeting/views/meeting_view.dart +++ b/lib/modules/meeting/views/meeting_view.dart @@ -21,500 +21,49 @@ class MeetingView extends GetView { child: Scaffold( backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], appBar: _buildAppBar(context, isDarkMode), - body: _buildBody(context, isDarkMode), - floatingActionButton: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, + body: Stack( children: [ - FloatingActionButton( - heroTag: "addAudio", - onPressed: () { - _showAddAudioMenu(context, isDarkMode); - }, - child: const Icon(Icons.add), - ), + _buildBody(context, isDarkMode), + _buildMicButton(context, isDarkMode), ], ), + floatingActionButton: _buildFloatingButtons(), ), ); } -// 底部+号菜单 - void _showAddAudioMenu(BuildContext context, bool isDarkMode) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) { - return Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom + 30.w, - left: 16.w, - right: 16.w, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 录音按钮 - Container( - margin: EdgeInsets.only(bottom: 12.w), - child: GestureDetector( - onTap: () { - Navigator.of(context).pop(); - // TODO: 实现开始录音逻辑 - _showStartsRecordSheet(context, isDarkMode); - }, - child: Container( - height: 56.w, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withOpacity(0.05) - : Colors.grey[100], - borderRadius: BorderRadius.circular(28.w), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 36.w, - height: 36.w, - decoration: BoxDecoration( - color: Colors.red.withOpacity(0.15), - shape: BoxShape.circle, - ), - child: Icon(Icons.mic, color: Colors.red, size: 24.w), - ), - SizedBox(width: 12.w), - Text( - '开始录音', - style: TextStyle( - color: isDarkMode ? Colors.white70 : Colors.black87, - fontSize: 16.sp, - fontWeight: FontWeight.w500, - ), - ), - Spacer(), - Icon(Icons.arrow_forward_ios, - color: Colors.grey, size: 18.w), - SizedBox(width: 16.w), - ], - ), - ), - ), - ), - // 导入音频按钮 - Container( - margin: EdgeInsets.only(bottom: 24.w), - child: GestureDetector( - onTap: () { - Navigator.of(context).pop(); - _showImportAudioSheet(context, isDarkMode); - }, - child: Container( - height: 56.w, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withOpacity(0.10) - : Colors.white, - borderRadius: BorderRadius.circular(28.w), - border: Border.all( - color: Colors.blue.withOpacity(0.15), - width: 2, - ), - boxShadow: [ - BoxShadow( - color: Colors.blue.withOpacity(0.08), - blurRadius: 8, - offset: Offset(0, 2), - ), - ], - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 36.w, - height: 36.w, - decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.15), - shape: BoxShape.circle, - ), - child: Icon(Icons.file_upload_outlined, - color: Colors.blue, size: 24.w), - ), - SizedBox(width: 12.w), - Text( - '导入音频', - style: TextStyle( - color: Colors.blue, - fontSize: 16.sp, - fontWeight: FontWeight.w500, - ), - ), - Spacer(), - Icon(Icons.arrow_forward_ios, - color: Colors.grey, size: 18.w), - SizedBox(width: 16.w), - ], - ), - ), - ), - ), - // 关闭按钮 - GestureDetector( - onTap: () => Navigator.of(context).pop(), - child: Container( - width: 56.w, - height: 56.w, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.white.withOpacity(0.08) - : Colors.black.withOpacity(0.08), - shape: BoxShape.circle, - ), - child: Icon(Icons.close, - color: isDarkMode ? Colors.white : Colors.black54, - size: 32.w), - ), - ), - SizedBox(height: 10.w), - ], - ), - ); - }, - ); - } - - // 新增:从opus导出选择弹窗 - void _showLocalOpusPicker(BuildContext context, bool isDarkMode) async { - // 这里以用户目录下的 Downloads 为例,可根据实际需求修改路径 - controller.loadTranslatopus(); - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) { - return StatefulBuilder( - builder: (context, setState) { - return Container( - decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[900] : Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)), - ), - padding: EdgeInsets.only( - left: 0, - right: 0, - top: 12.w, - bottom: MediaQuery.of(context).viewInsets.bottom + 12.w, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '选择音视频文件', - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - Divider( - height: 1, - color: isDarkMode ? Colors.white12 : Colors.grey[200]), - SizedBox(height: 8.w), - SizedBox( - height: 300.w, - child: ListView.builder( - itemCount: controller.files.length, - itemBuilder: (context, idx) { - final file = controller.files[idx]; - final fileName = file.path.split('/').last; - final isChecked = - controller.selected.contains(file.path); - return CheckboxListTile( - value: isChecked, - onChanged: (checked) { - setState(() { - if (checked == true) { - controller.selected.add(file.path); - } else { - controller.selected.remove(file.path); - } - }); - }, - title: Text( - fileName, - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black87, - fontSize: 15.sp, - ), - ), - controlAffinity: ListTileControlAffinity.leading, - activeColor: Colors.blue, - ); - }, - ), - ), - SizedBox(height: 12.w), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.grey, - ), - onPressed: () => Navigator.of(context).pop(), - child: - Text('取消', style: TextStyle(color: Colors.white)), - ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - ), - onPressed: controller.selected.isEmpty - ? null - : () { - Navigator.of(context).pop(); - // TODO: 调用导入逻辑 - controller.shareAllTranslatAudio(); - }, - child: - Text('导入选中', style: TextStyle(color: Colors.white)), - ), - ], - ), - SizedBox(height: 8.w), - ], - ), - ); + /// 构建底部中间的录音按钮 + Widget _buildMicButton(BuildContext context, bool isDarkMode) { + return Positioned( + bottom: 20, + left: 0, + right: 0, + child: Center( + child: FloatingActionButton( + heroTag: "recordAudio", + onPressed: () { + _showStartsRecordSheet(context, isDarkMode); }, - ); - }, + child: const Icon(Icons.mic), + ), + ), ); } -// 新增:本地音视频文件选择弹窗 - void _showLocalAudioVideoPicker(BuildContext context, bool isDarkMode) async { - // 这里以用户目录下的 Downloads 为例,可根据实际需求修改路径 - controller.loadTranslatAudio(); - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) { - return StatefulBuilder( - builder: (context, setState) { - return Container( - decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[900] : Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)), - ), - padding: EdgeInsets.only( - left: 0, - right: 0, - top: 12.w, - bottom: MediaQuery.of(context).viewInsets.bottom + 12.w, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '选择音视频文件', - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - Divider( - height: 1, - color: isDarkMode ? Colors.white12 : Colors.grey[200]), - SizedBox(height: 8.w), - SizedBox( - height: 300.w, - child: ListView.builder( - itemCount: controller.files.length, - itemBuilder: (context, idx) { - final file = controller.files[idx]; - final fileName = file.path.split('/').last; - final isChecked = - controller.selected.contains(file.path); - return CheckboxListTile( - value: isChecked, - onChanged: (checked) { - setState(() { - if (checked == true) { - controller.selected.add(file.path); - } else { - controller.selected.remove(file.path); - } - }); - }, - title: Text( - fileName, - style: TextStyle( - color: isDarkMode ? Colors.white : Colors.black87, - fontSize: 15.sp, - ), - ), - controlAffinity: ListTileControlAffinity.leading, - activeColor: Colors.blue, - ); - }, - ), - ), - SizedBox(height: 12.w), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.grey, - ), - onPressed: () => Navigator.of(context).pop(), - child: - Text('取消', style: TextStyle(color: Colors.white)), - ), - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blue, - ), - onPressed: controller.selected.isEmpty - ? null - : () { - Navigator.of(context).pop(); - // TODO: 调用导入逻辑 - controller.addTranslatAudio(); - }, - child: - Text('导入选中', style: TextStyle(color: Colors.white)), - ), - ], - ), - SizedBox(height: 8.w), - ], - ), - ); + /// 构建右下角的悬浮按钮 + Widget _buildFloatingButtons() { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + FloatingActionButton( + heroTag: "addAudio", + onPressed: () { + controller.addAudio(); }, - ); - }, - ); - } - -// 导入音频弹窗 - void _showImportAudioSheet(BuildContext context, bool isDarkMode) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) { - return Container( - decoration: BoxDecoration( - color: isDarkMode ? Colors.grey[900] : Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)), - ), - padding: EdgeInsets.only( - left: 0, - right: 0, - top: 12.w, - bottom: MediaQuery.of(context).viewInsets.bottom + 12.w, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - alignment: Alignment.centerLeft, - padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 8.w), - child: Text( - '导入音频', - style: TextStyle( - fontSize: 18.sp, - fontWeight: FontWeight.bold, - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - ), - Divider( - height: 1, - color: isDarkMode ? Colors.white12 : Colors.grey[200]), - // 选项列表 - Padding( - padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.w), - child: Column( - children: [ - _importAudioItem1( - context, - isDarkMode, - icon: Image.asset('assets/images/explore.png', - width: 32.w, height: 32.w), - title: '从外部导入', - onTap: () { - Navigator.of(context).pop(); - controller.addAudio(); - // TODO: 实现从文件导入逻辑 - }, - ), - // SizedBox(height: 8.w), - // _importAudioItem1( - // context, - // isDarkMode, - // icon: Image.asset('assets/images/explore.png', - // width: 32.w, height: 32.w), - // title: '从翻译里导入', - // onTap: () { - // Navigator.of(context).pop(); - // _showLocalAudioVideoPicker(context, isDarkMode); - // // TODO: 实现从相册导入逻辑 - // }, - // ), - // SizedBox(height: 8.w), - // _importAudioItem1( - // context, - // isDarkMode, - // icon: Row( - // mainAxisSize: MainAxisSize.min, - // children: [ - // Image.asset('assets/images/explore.png', - // width: 20.w, height: 20.w), - // SizedBox(width: 4.w), - // Image.asset('assets/images/explore.png', - // width: 20.w, height: 20.w), - // SizedBox(width: 4.w), - // Image.asset('assets/images/explore.png', - // width: 20.w, height: 20.w), - // ], - // ), - // title: '从opus导出', - // onTap: () { - // Navigator.of(context).pop(); - // // TODO: 实现从其他APP导入逻辑 - // _showLocalOpusPicker(context, isDarkMode); - // }, - // ), - ], - ), - ), - SizedBox(height: 8.w), - Padding( - padding: EdgeInsets.symmetric(horizontal: 20.w), - child: Text( - '支持导入单文件时长不超过5小时的音频', - style: TextStyle( - fontSize: 13.sp, - color: isDarkMode ? Colors.white54 : Colors.grey[600], - ), - ), - ), - SizedBox(height: 16.w), - GestureDetector( - onTap: () => Navigator.of(context).pop(), - child: Icon(Icons.close, size: 32.w, color: Colors.grey), - ), - SizedBox(height: 8.w), - ], - ), - ); - }, + child: const Icon(Icons.add), + ), + ], ); } @@ -695,47 +244,6 @@ class MeetingView extends GetView { ); } -// 选项框 - Widget _importAudioItem1( - BuildContext context, - bool isDarkMode, { - required Widget icon, - required String title, - required VoidCallback onTap, - }) { - return Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(12.w), - onTap: onTap, - child: Container( - height: 52.w, - padding: EdgeInsets.symmetric(horizontal: 12.w), - decoration: BoxDecoration( - color: - isDarkMode ? Colors.white.withOpacity(0.03) : Colors.grey[50], - borderRadius: BorderRadius.circular(12.w), - ), - child: Row( - children: [ - icon, - SizedBox(width: 14.w), - Expanded( - child: Text( - title, - style: TextStyle( - fontSize: 16.sp, - color: isDarkMode ? Colors.white : Colors.black87, - ), - ), - ), - Icon(Icons.arrow_forward_ios, size: 18.w, color: Colors.grey), - ], - ), - ), - ), - ); - } // 新增右侧弹窗 void _showRightDrawerDialog(BuildContext context, bool isDarkMode) { @@ -1219,7 +727,528 @@ class MeetingView extends GetView { clipBehavior: Clip.hardEdge, ); } + // ...existing code... +// // 选项框 +// Widget _importAudioItem1( +// BuildContext context, +// bool isDarkMode, { +// required Widget icon, +// required String title, +// required VoidCallback onTap, +// }) { +// return Material( +// color: Colors.transparent, +// child: InkWell( +// borderRadius: BorderRadius.circular(12.w), +// onTap: onTap, +// child: Container( +// height: 52.w, +// padding: EdgeInsets.symmetric(horizontal: 12.w), +// decoration: BoxDecoration( +// color: +// isDarkMode ? Colors.white.withOpacity(0.03) : Colors.grey[50], +// borderRadius: BorderRadius.circular(12.w), +// ), +// child: Row( +// children: [ +// icon, +// SizedBox(width: 14.w), +// Expanded( +// child: Text( +// title, +// style: TextStyle( +// fontSize: 16.sp, +// color: isDarkMode ? Colors.white : Colors.black87, +// ), +// ), +// ), +// Icon(Icons.arrow_forward_ios, size: 18.w, color: Colors.grey), +// ], +// ), +// ), +// ), +// ); +// } + +// // 底部+号菜单 +// void _showAddAudioMenu(BuildContext context, bool isDarkMode) { +// showModalBottomSheet( +// context: context, +// isScrollControlled: true, +// backgroundColor: Colors.transparent, +// builder: (context) { +// return Padding( +// padding: EdgeInsets.only( +// bottom: MediaQuery.of(context).viewInsets.bottom + 30.w, +// left: 16.w, +// right: 16.w, +// ), +// child: Column( +// mainAxisSize: MainAxisSize.min, +// children: [ +// // 录音按钮 +// Container( +// margin: EdgeInsets.only(bottom: 12.w), +// child: GestureDetector( +// onTap: () { +// Navigator.of(context).pop(); +// // TODO: 实现开始录音逻辑 +// _showStartsRecordSheet(context, isDarkMode); +// }, +// child: Container( +// height: 56.w, +// decoration: BoxDecoration( +// color: isDarkMode +// ? Colors.white.withOpacity(0.05) +// : Colors.grey[100], +// borderRadius: BorderRadius.circular(28.w), +// ), +// child: Row( +// mainAxisAlignment: MainAxisAlignment.center, +// children: [ +// Container( +// width: 36.w, +// height: 36.w, +// decoration: BoxDecoration( +// color: Colors.red.withOpacity(0.15), +// shape: BoxShape.circle, +// ), +// child: Icon(Icons.mic, color: Colors.red, size: 24.w), +// ), +// SizedBox(width: 12.w), +// Text( +// '开始录音', +// style: TextStyle( +// color: isDarkMode ? Colors.white70 : Colors.black87, +// fontSize: 16.sp, +// fontWeight: FontWeight.w500, +// ), +// ), +// Spacer(), +// Icon(Icons.arrow_forward_ios, +// color: Colors.grey, size: 18.w), +// SizedBox(width: 16.w), +// ], +// ), +// ), +// ), +// ), +// // 导入音频按钮 +// Container( +// margin: EdgeInsets.only(bottom: 24.w), +// child: GestureDetector( +// onTap: () { +// Navigator.of(context).pop(); +// _showImportAudioSheet(context, isDarkMode); +// }, +// child: Container( +// height: 56.w, +// decoration: BoxDecoration( +// color: isDarkMode +// ? Colors.white.withOpacity(0.10) +// : Colors.white, +// borderRadius: BorderRadius.circular(28.w), +// border: Border.all( +// color: Colors.blue.withOpacity(0.15), +// width: 2, +// ), +// boxShadow: [ +// BoxShadow( +// color: Colors.blue.withOpacity(0.08), +// blurRadius: 8, +// offset: Offset(0, 2), +// ), +// ], +// ), +// child: Row( +// mainAxisAlignment: MainAxisAlignment.center, +// children: [ +// Container( +// width: 36.w, +// height: 36.w, +// decoration: BoxDecoration( +// color: Colors.blue.withOpacity(0.15), +// shape: BoxShape.circle, +// ), +// child: Icon(Icons.file_upload_outlined, +// color: Colors.blue, size: 24.w), +// ), +// SizedBox(width: 12.w), +// Text( +// '导入音频', +// style: TextStyle( +// color: Colors.blue, +// fontSize: 16.sp, +// fontWeight: FontWeight.w500, +// ), +// ), +// Spacer(), +// Icon(Icons.arrow_forward_ios, +// color: Colors.grey, size: 18.w), +// SizedBox(width: 16.w), +// ], +// ), +// ), +// ), +// ), +// // 关闭按钮 +// GestureDetector( +// onTap: () => Navigator.of(context).pop(), +// child: Container( +// width: 56.w, +// height: 56.w, +// decoration: BoxDecoration( +// color: isDarkMode +// ? Colors.white.withOpacity(0.08) +// : Colors.black.withOpacity(0.08), +// shape: BoxShape.circle, +// ), +// child: Icon(Icons.close, +// color: isDarkMode ? Colors.white : Colors.black54, +// size: 32.w), +// ), +// ), +// SizedBox(height: 10.w), +// ], +// ), +// ); +// }, +// ); +// } + +// // 新增:从opus导出选择弹窗 +// void _showLocalOpusPicker(BuildContext context, bool isDarkMode) async { +// // 这里以用户目录下的 Downloads 为例,可根据实际需求修改路径 +// controller.loadTranslatopus(); +// showModalBottomSheet( +// context: context, +// isScrollControlled: true, +// backgroundColor: Colors.transparent, +// builder: (context) { +// return StatefulBuilder( +// builder: (context, setState) { +// return Container( +// decoration: BoxDecoration( +// color: isDarkMode ? Colors.grey[900] : Colors.white, +// borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)), +// ), +// padding: EdgeInsets.only( +// left: 0, +// right: 0, +// top: 12.w, +// bottom: MediaQuery.of(context).viewInsets.bottom + 12.w, +// ), +// child: Column( +// mainAxisSize: MainAxisSize.min, +// children: [ +// Text( +// '选择音视频文件', +// style: TextStyle( +// fontSize: 18.sp, +// fontWeight: FontWeight.bold, +// color: isDarkMode ? Colors.white : Colors.black87, +// ), +// ), +// Divider( +// height: 1, +// color: isDarkMode ? Colors.white12 : Colors.grey[200]), +// SizedBox(height: 8.w), +// SizedBox( +// height: 300.w, +// child: ListView.builder( +// itemCount: controller.files.length, +// itemBuilder: (context, idx) { +// final file = controller.files[idx]; +// final fileName = file.path.split('/').last; +// final isChecked = +// controller.selected.contains(file.path); +// return CheckboxListTile( +// value: isChecked, +// onChanged: (checked) { +// setState(() { +// if (checked == true) { +// controller.selected.add(file.path); +// } else { +// controller.selected.remove(file.path); +// } +// }); +// }, +// title: Text( +// fileName, +// style: TextStyle( +// color: isDarkMode ? Colors.white : Colors.black87, +// fontSize: 15.sp, +// ), +// ), +// controlAffinity: ListTileControlAffinity.leading, +// activeColor: Colors.blue, +// ); +// }, +// ), +// ), +// SizedBox(height: 12.w), +// Row( +// mainAxisAlignment: MainAxisAlignment.spaceEvenly, +// children: [ +// ElevatedButton( +// style: ElevatedButton.styleFrom( +// backgroundColor: Colors.grey, +// ), +// onPressed: () => Navigator.of(context).pop(), +// child: +// Text('取消', style: TextStyle(color: Colors.white)), +// ), +// ElevatedButton( +// style: ElevatedButton.styleFrom( +// backgroundColor: Colors.blue, +// ), +// onPressed: controller.selected.isEmpty +// ? null +// : () { +// Navigator.of(context).pop(); +// // TODO: 调用导入逻辑 +// controller.shareAllTranslatAudio(); +// }, +// child: +// Text('导入选中', style: TextStyle(color: Colors.white)), +// ), +// ], +// ), +// SizedBox(height: 8.w), +// ], +// ), +// ); +// }, +// ); +// }, +// ); +// } + +// // 新增:本地音视频文件选择弹窗 +// void _showLocalAudioVideoPicker(BuildContext context, bool isDarkMode) async { +// // 这里以用户目录下的 Downloads 为例,可根据实际需求修改路径 +// controller.loadTranslatAudio(); +// showModalBottomSheet( +// context: context, +// isScrollControlled: true, +// backgroundColor: Colors.transparent, +// builder: (context) { +// return StatefulBuilder( +// builder: (context, setState) { +// return Container( +// decoration: BoxDecoration( +// color: isDarkMode ? Colors.grey[900] : Colors.white, +// borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)), +// ), +// padding: EdgeInsets.only( +// left: 0, +// right: 0, +// top: 12.w, +// bottom: MediaQuery.of(context).viewInsets.bottom + 12.w, +// ), +// child: Column( +// mainAxisSize: MainAxisSize.min, +// children: [ +// Text( +// '选择音视频文件', +// style: TextStyle( +// fontSize: 18.sp, +// fontWeight: FontWeight.bold, +// color: isDarkMode ? Colors.white : Colors.black87, +// ), +// ), +// Divider( +// height: 1, +// color: isDarkMode ? Colors.white12 : Colors.grey[200]), +// SizedBox(height: 8.w), +// SizedBox( +// height: 300.w, +// child: ListView.builder( +// itemCount: controller.files.length, +// itemBuilder: (context, idx) { +// final file = controller.files[idx]; +// final fileName = file.path.split('/').last; +// final isChecked = +// controller.selected.contains(file.path); +// return CheckboxListTile( +// value: isChecked, +// onChanged: (checked) { +// setState(() { +// if (checked == true) { +// controller.selected.add(file.path); +// } else { +// controller.selected.remove(file.path); +// } +// }); +// }, +// title: Text( +// fileName, +// style: TextStyle( +// color: isDarkMode ? Colors.white : Colors.black87, +// fontSize: 15.sp, +// ), +// ), +// controlAffinity: ListTileControlAffinity.leading, +// activeColor: Colors.blue, +// ); +// }, +// ), +// ), +// SizedBox(height: 12.w), +// Row( +// mainAxisAlignment: MainAxisAlignment.spaceEvenly, +// children: [ +// ElevatedButton( +// style: ElevatedButton.styleFrom( +// backgroundColor: Colors.grey, +// ), +// onPressed: () => Navigator.of(context).pop(), +// child: +// Text('取消', style: TextStyle(color: Colors.white)), +// ), +// ElevatedButton( +// style: ElevatedButton.styleFrom( +// backgroundColor: Colors.blue, +// ), +// onPressed: controller.selected.isEmpty +// ? null +// : () { +// Navigator.of(context).pop(); +// // TODO: 调用导入逻辑 +// controller.addTranslatAudio(); +// }, +// child: +// Text('导入选中', style: TextStyle(color: Colors.white)), +// ), +// ], +// ), +// SizedBox(height: 8.w), +// ], +// ), +// ); +// }, +// ); +// }, +// ); +// } + +// // 导入音频弹窗 +// void _showImportAudioSheet(BuildContext context, bool isDarkMode) { +// showModalBottomSheet( +// context: context, +// isScrollControlled: true, +// backgroundColor: Colors.transparent, +// builder: (context) { +// return Container( +// decoration: BoxDecoration( +// color: isDarkMode ? Colors.grey[900] : Colors.white, +// borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)), +// ), +// padding: EdgeInsets.only( +// left: 0, +// right: 0, +// top: 12.w, +// bottom: MediaQuery.of(context).viewInsets.bottom + 12.w, +// ), +// child: Column( +// mainAxisSize: MainAxisSize.min, +// children: [ +// Container( +// alignment: Alignment.centerLeft, +// padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 8.w), +// child: Text( +// '导入音频', +// style: TextStyle( +// fontSize: 18.sp, +// fontWeight: FontWeight.bold, +// color: isDarkMode ? Colors.white : Colors.black87, +// ), +// ), +// ), +// Divider( +// height: 1, +// color: isDarkMode ? Colors.white12 : Colors.grey[200]), +// // 选项列表 +// Padding( +// padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.w), +// child: Column( +// children: [ +// _importAudioItem1( +// context, +// isDarkMode, +// icon: Image.asset('assets/images/explore.png', +// width: 32.w, height: 32.w), +// title: '从外部导入', +// onTap: () { +// Navigator.of(context).pop(); +// controller.addAudio(); +// // TODO: 实现从文件导入逻辑 +// }, +// ), +// // SizedBox(height: 8.w), +// // _importAudioItem1( +// // context, +// // isDarkMode, +// // icon: Image.asset('assets/images/explore.png', +// // width: 32.w, height: 32.w), +// // title: '从翻译里导入', +// // onTap: () { +// // Navigator.of(context).pop(); +// // _showLocalAudioVideoPicker(context, isDarkMode); +// // // TODO: 实现从相册导入逻辑 +// // }, +// // ), +// // SizedBox(height: 8.w), +// // _importAudioItem1( +// // context, +// // isDarkMode, +// // icon: Row( +// // mainAxisSize: MainAxisSize.min, +// // children: [ +// // Image.asset('assets/images/explore.png', +// // width: 20.w, height: 20.w), +// // SizedBox(width: 4.w), +// // Image.asset('assets/images/explore.png', +// // width: 20.w, height: 20.w), +// // SizedBox(width: 4.w), +// // Image.asset('assets/images/explore.png', +// // width: 20.w, height: 20.w), +// // ], +// // ), +// // title: '从opus导出', +// // onTap: () { +// // Navigator.of(context).pop(); +// // // TODO: 实现从其他APP导入逻辑 +// // _showLocalOpusPicker(context, isDarkMode); +// // }, +// // ), +// ], +// ), +// ), +// SizedBox(height: 8.w), +// Padding( +// padding: EdgeInsets.symmetric(horizontal: 20.w), +// child: Text( +// '支持导入单文件时长不超过5小时的音频', +// style: TextStyle( +// fontSize: 13.sp, +// color: isDarkMode ? Colors.white54 : Colors.grey[600], +// ), +// ), +// ), +// SizedBox(height: 16.w), +// GestureDetector( +// onTap: () => Navigator.of(context).pop(), +// child: Icon(Icons.close, size: 32.w, color: Colors.grey), +// ), +// SizedBox(height: 8.w), +// ], +// ), +// ); +// }, +// ); +// } // void _showRecordingSheet(BuildContext context, bool isDarkMode) { // if (_recordingOverlayEntry != null) return; // 防止重复添加 diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index d83b04bdd..dd307a50f 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -17,6 +17,13 @@ import '../../../data/models/events/voice_interaction_event.dart'; import '../../../data/services/asr_service.dart'; import '../../../data/services/ble_manager.dart'; +// 切换音频输出设备的枚举 +enum AudioOutputDevice { + defaultDevice, // 默认(如有耳机优先耳机,否则扬声器) + headphones, // 强制耳机 + speaker, // 强制扬声器 +} + class TranslationController extends GetxController { // 服务 final AsrService _asrService = Get.find(); @@ -55,6 +62,7 @@ class TranslationController extends GetxController { final appDocDir = getApplicationDocumentsDirectory(); var appDir; var dir; + Future changeTranslationMode(String mode) async { currentMode.value = mode; currentModeTitle.value = _getModeTitle(mode); @@ -104,7 +112,7 @@ class TranslationController extends GetxController { //_setupEventListeners(); // 加载上次保存的语言设置 _loadSavedLanguages(); - _requestStoragePermission(); + // 初始化语言代码 final sourceAsrCode = _languageManager.getAsrCodeByChineseName(sourceLanguage.value); @@ -175,25 +183,27 @@ class TranslationController extends GetxController { } // 请求存储权限 - void _requestStoragePermission() async { + Future _requestStoragePermission() async { // final BuildContext context = Get.overlayContext!; + try { + final bool granted = await PermissionUtil.instance.requestPermission( + permissionType: Permission.storage, + permissionName: "存储", + explanationText: "我们需要访问存储权限,以便您可以选择并上传音频文件。", + permanentDenialText: "检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。", + ); - PermissionUtil.instance - .requestPermission( - // context: context, - permissionType: Permission.storage, - permissionName: "存储", - explanationText: "我们需要访问存储权限,以便您可以选择并上传音频文件。", - permanentDenialText: "检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。", - ) - .then((granted) { if (granted) { Logger.d("Permission", "用户授予了存储权限"); + return true; } else { Logger.d("Permission", "用户未授予存储权限"); - // 可以提示用户无法进行文件操作 + return false; } - }); + } catch (e) { + Logger.e("Permission", "请求存储权限时出错: $e"); + return false; + } } // /// 设置事件监听器 // void _setupEventListeners() { @@ -224,6 +234,11 @@ class TranslationController extends GetxController { await _asrService.initialize( supportedLanguages: asrSupportedLanguages, ); + final bool storage = await _requestStoragePermission(); + if (!storage) { + Logger.d("Permission", "未授予存储权限,无法提供录音功能"); + return; + } appDir = await appDocDir; // 先 await 获取目录 dir = Directory("${appDir.path}/TranslatAudio"); if (!await dir.exists()) { @@ -266,10 +281,10 @@ class TranslationController extends GetxController { Future startRecognition() async { Logger.info('开始语音识别'); // 等待权限请求完成并获取结果 - final bool granted = await _requestRecordPermission(); + final bool record = await _requestRecordPermission(); // 如果权限被拒绝,直接返回 - if (!granted) { + if (!record) { Logger.d("Permission", "录音权限被拒绝,无法开始录音"); return; } @@ -328,6 +343,11 @@ class TranslationController extends GetxController { await _asrService.startContinuousRecognition(_audioSourceType); isRecognizing.value = true; if (isRecording.value && isRecognizing.value) { + final bool storage = await _requestStoragePermission(); + if (!storage) { + Logger.d("Permission", "未授予存储权限,无法提供录音功能"); + return; + } final formattedTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now()); await _asrService.enableRecord( @@ -516,15 +536,32 @@ class TranslationController extends GetxController { detectedSourceLanguageCode; translationHistory[index].targetLanguageCode = detectedTargetLanguageCode; - translationHistory.refresh(); - saveTranslationHistory(); - // 更新后滚动到底部 - _scrollToBottom(); - - // 播放TTS - 只在最终结果时播放 - if (isTtsEnabled.value && isFinal) { - // Logger.info('播放翻译文本: ${translationResult}'); - await playTranslatedText(translationResult); + print("判断是否是面对面模式且语言一致: ${detectedSourceLanguageCode}"); + // 判断是否是面对面模式且语言一致 + final bool isFaceToFaceSameLang = + detectedSourceLanguageCode == sourceLanguageCode.value && + currentMode.value == "faceToFace"; + + // 设置音频输出设备 + var isSuccess = await _ttsService.setAudioOutputDevice( + isFaceToFaceSameLang + ? AudioOutputDevice.speaker.index + : AudioOutputDevice.headphones.index, + ); + if (isSuccess) { + print('设置音频输出设备成功'); + translationHistory.refresh(); + saveTranslationHistory(); + // 更新后滚动到底部 + _scrollToBottom(); + + // 播放TTS - 只在最终结果时播放 + if (isTtsEnabled.value && isFinal) { + // Logger.info('播放翻译文本: ${translationResult}'); + await playTranslatedText(translationResult); + } + } else { + print('设置音频输出设备失败'); } } else { Logger.info( diff --git a/lib/modules/translation/views/translation_list_view.dart b/lib/modules/translation/views/translation_list_view.dart index a24f56c01..994882077 100644 --- a/lib/modules/translation/views/translation_list_view.dart +++ b/lib/modules/translation/views/translation_list_view.dart @@ -278,15 +278,16 @@ class TranslationListView extends GetView { ), _popupMenuEntry( Icon( - Icons.delete_outline, + Icons.smart_button, size: 20.w, - color: isDarkMode ? Colors.red[700] : Colors.red, + color: isDarkMode ? Colors.white : Colors.black87, ), - '删除', - isDarkMode ? Colors.red[700]! : Colors.red, + '智能转写', + isDarkMode ? Colors.white : Colors.black87, () { - file.delete(); - controller.loadTranslatAudio(); // 刷新列表 + Get.toNamed(Routes.meeting, arguments: { + 'filePath': file.path, + }); }, ), _popupMenuEntry( @@ -295,12 +296,11 @@ class TranslationListView extends GetView { size: 20.w, color: isDarkMode ? Colors.red[700] : Colors.red, ), - '导入', + '删除', isDarkMode ? Colors.red[700]! : Colors.red, () { - Get.toNamed(Routes.meeting, arguments: { - 'filePath': file.path, - }); + file.delete(); + controller.loadTranslatAudio(); // 刷新列表 }, ), ], diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index 048f5f463..3a9df2239 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -110,7 +110,9 @@ class TranslationView extends GetView { // TTS toggle button Obx(() => IconButton( icon: Icon( - controller.isRecording.value ? Icons.mic : Icons.mic_off, + controller.isRecording.value + ? Icons.mic + : Icons.fiber_manual_record, color: isDarkMode ? Colors.white : Colors.black, size: 24.sp, ), @@ -122,7 +124,7 @@ class TranslationView extends GetView { // 历史记录按钮 IconButton( icon: Icon( - Icons.history, + Icons.folder, color: isDarkMode ? Colors.white : Colors.black, size: 24.sp, ), @@ -134,7 +136,7 @@ class TranslationView extends GetView { // 返回时不自动开始翻译,只需重新设置滚动监听器 controller.setupScrollListener(); }, - tooltip: '查看历史记录', + tooltip: '查看录音文件', ), ], bottom: PreferredSize( diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index 9b21bd39a..f38f5e31e 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -147,13 +147,14 @@ class AzureAsrHelper(private val context: Context) { } // 录音文件类 recordfile = RecordFile; + //进入界面手机麦克风就被占用,导致其他app无法使用麦克风,在开始录音再去申请 // 创建识别器 - val setupSuccess = setupRecognizer() - if (setupSuccess) { - // 优化:初始化完成后进行预热 - warmupRecognizer() - } - return setupSuccess + // val setupSuccess = setupRecognizer() + // if (setupSuccess) { + // // 优化:初始化完成后进行预热 + // warmupRecognizer() + // } + return true } catch (e: Exception) { Log.e(tag, "初始化失败: ${e.message}") return false diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt index 498613b8c..741605be7 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt @@ -502,7 +502,19 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { result.success(true) } "setAudioOutputDevice" -> { - azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.SPEAKER) + val type = call.argument("type") + if (type == 0) { + // 默认(如果有耳机选耳机,否则使用系统扬声器) + azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.DEFAULT) + } else if (type == 1) { + // 强制使用耳机 + azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.HEADPHONES) + } else if (type == 2) { + // 强制使用扬声器 + azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.SPEAKER) + } + + result.success(true) } diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt index d5444ffb2..f0fb6f08a 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt @@ -213,6 +213,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService { override fun setAudioOutputDevice(device: AudioOutputDevice) { // Azure TTS使用系统默认的音频路由 // 音频输出设备的控制需要通过Android的AudioManager实现 + FileLogger.e(TAG, "音频输出设备类型device=: ${device}") val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager audioManager?.let { manager -> when (device) {