From a5233a544d1a80f4b579dd927f1523a05d354125 Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Mon, 23 Jun 2025 13:03:56 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BC=9A=E8=AE=AE?= =?UTF-8?q?=E5=8A=A9=E6=89=8B=E6=9B=B4=E5=A4=9A=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/utils/synchrodata.dart | 33 +++--- .../meeting/views/generate_bottom_sheet.dart | 101 ++++++++++-------- .../meeting/views/meeting_details_view.dart | 14 ++- .../meeting/views/more_bottom_sheet.dart | 92 ++++++++++++++++ 4 files changed, 179 insertions(+), 61 deletions(-) create mode 100644 lib/modules/meeting/views/more_bottom_sheet.dart diff --git a/lib/core/utils/synchrodata.dart b/lib/core/utils/synchrodata.dart index 4d8d2e3af..3e3148829 100644 --- a/lib/core/utils/synchrodata.dart +++ b/lib/core/utils/synchrodata.dart @@ -8,7 +8,11 @@ import '../../data/services/network/api.dart'; class Synchrodata { static final GetStorage _storage = GetStorage(); - static List _records = _storage.read("operation_records") ?? []; + static List> _records = + (_storage.read("operation_records") as List?) + ?.map((e) => Map.from(e as Map)) + .toList() ?? + []; static List _delids = _storage.read("operation_delids") ?? []; static bool _isSynchro = false; @@ -30,7 +34,7 @@ class Synchrodata { switch (method) { case 'insert': _records.add({ - ...params, + ...Map.from(params), 'id': id, 'number': params['id'], 'uid': userId, @@ -38,19 +42,19 @@ class Synchrodata { _storage.write("operation_records", _records); break; case 'update': - Map? foundElement = _records.firstWhere( + Map? foundElement = _records.firstWhere( (element) => element['id'] == id, - orElse: () => null, + orElse: () => {}, ); - if (foundElement != null) { + if (foundElement.isNotEmpty) { foundElement.addAll({ - ...params, + ...Map.from(params), 'id': id, 'number': params['id'], }); } else { _records.add({ - ...params, + ...Map.from(params), 'id': id, 'number': params['id'], 'uid': userId, @@ -75,15 +79,18 @@ class Synchrodata { // 获取需要修改的全部数据 List putRecords = []; for (var element in _records) { - final result = await SqfliteApi.getMeetingDetails(element['number']); - final speakerList = - await SqfliteApi.getMeetingSpeaker(element['number']); + Map result = {'details': {}}; + List speakerList = []; + if (!_delids.contains(element['id'])) { + result = await SqfliteApi.getMeetingDetails(element['number']); + speakerList = await SqfliteApi.getMeetingSpeaker(element['number']); + } putRecords.add({ - ...result, - ...result['details'], + ...Map.from(result), + ...Map.from(result['details']), 'rtype': result['type'], 'original': jsonEncode(speakerList), - ...element, + ...Map.from(element), }); } var result = await Api.putOperationRecord({ diff --git a/lib/modules/meeting/views/generate_bottom_sheet.dart b/lib/modules/meeting/views/generate_bottom_sheet.dart index 3343357b2..1e2c8568c 100644 --- a/lib/modules/meeting/views/generate_bottom_sheet.dart +++ b/lib/modules/meeting/views/generate_bottom_sheet.dart @@ -9,7 +9,9 @@ import 'record_language_bottom_sheet.dart'; import 'template_view.dart'; class GenerateBottomSheet extends StatefulWidget { - const GenerateBottomSheet({super.key}); + final bool isSummarizeAgain; + + const GenerateBottomSheet({super.key, this.isSummarizeAgain = false}); @override State createState() => _GenerateBottomSheetState(); @@ -214,56 +216,57 @@ class _GenerateBottomSheetState extends State { }, ), ), - Container( - margin: EdgeInsets.symmetric(horizontal: 12.w), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(8.r), - ), - child: Column( - children: [ - _row( - '区分说话人', - widget: Transform.scale( - scale: 0.8, - alignment: Alignment.centerRight, - child: Switch( - value: _isSpeaker, - onChanged: (bool value) { - setState(() { - _isSpeaker = value; - }); - }, + if (!widget.isSummarizeAgain) + Container( + margin: EdgeInsets.symmetric(horizontal: 12.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8.r), + ), + child: Column( + children: [ + _row( + '区分说话人', + widget: Transform.scale( + scale: 0.8, + alignment: Alignment.centerRight, + child: Switch( + value: _isSpeaker, + onChanged: (bool value) { + setState(() { + _isSpeaker = value; + }); + }, + ), ), ), - ), - _row( - '录音语言', - value: _languageText, - isBorder: false, - onTap: () { - Get.bottomSheet( - RecordLanguageBottomSheet(_language), - isScrollControlled: true, - backgroundColor: Colors.grey[100], - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(12.r), + _row( + '录音语言', + value: _languageText, + isBorder: false, + onTap: () { + Get.bottomSheet( + RecordLanguageBottomSheet(_language), + isScrollControlled: true, + backgroundColor: Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), ), - ), - ).then((value) { - if (value != null) { - setState(() { - _languageText = value['name']; - _language = value['value']; - }); - } - }); - }, - ), - ], + ).then((value) { + if (value != null) { + setState(() { + _languageText = value['name']; + _language = value['value']; + }); + } + }); + }, + ), + ], + ), ), - ), Padding( padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 20.w), child: GestureDetector( @@ -279,6 +282,10 @@ class _GenerateBottomSheetState extends State { _storage.write("meeting_last_template_Data", _templateData); _storage.write("meeting_template_list", _templateList); Get.back(); + if (widget.isSummarizeAgain) { + _controller.meetingDetails.value.tasktype = 2; + _controller.meetingDetails.refresh(); + } _controller.summaryCreate(); }, child: Container( diff --git a/lib/modules/meeting/views/meeting_details_view.dart b/lib/modules/meeting/views/meeting_details_view.dart index de44350a7..1ab644bc3 100644 --- a/lib/modules/meeting/views/meeting_details_view.dart +++ b/lib/modules/meeting/views/meeting_details_view.dart @@ -10,6 +10,7 @@ import 'package:lottie/lottie.dart'; import '../../../core/theme/app_colors.dart'; import '../controllers/meeting_details_controller.dart'; import 'mind_map_tab.dart'; +import 'more_bottom_sheet.dart'; import 'overview_tab.dart'; import 'share_bottom_sheet.dart'; import 'speech_tab.dart'; @@ -189,7 +190,18 @@ class MeetingDetailsView extends GetView { size: 16.sp, ), ), - onPressed: () {}, + onPressed: () { + Get.bottomSheet( + const MoreBottomSheet(), + isScrollControlled: true, + backgroundColor: Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + ); + }, ), ], ); diff --git a/lib/modules/meeting/views/more_bottom_sheet.dart b/lib/modules/meeting/views/more_bottom_sheet.dart new file mode 100644 index 000000000..38e96f967 --- /dev/null +++ b/lib/modules/meeting/views/more_bottom_sheet.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +import '../controllers/meeting_controller.dart'; +import '../controllers/meeting_details_controller.dart'; +import 'generate_bottom_sheet.dart'; + +class MoreBottomSheet extends StatefulWidget { + const MoreBottomSheet({super.key}); + + @override + State createState() => _MoreBottomSheetState(); +} + +class _MoreBottomSheetState extends State { + final _controller = Get.find(); + final _detailsController = Get.find(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: 10.w), + if (_detailsController.meetingDetails.value.tasktype == 3) + _shareItem(Icons.refresh, '重新总结', onTap: () { + Get.back(); + Get.bottomSheet( + const GenerateBottomSheet( + isSummarizeAgain: true, + ), + isScrollControlled: true, + backgroundColor: Colors.grey[100], + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(12.r), + ), + ), + ); + }), + _shareItem(Icons.delete_outline, '删除', onTap: () async { + _controller.deleteMeeting( + _detailsController.meetingData.value.id, + _detailsController.meetingData.value.filepath, + ); + Get.back(); + Get.back(); + }), + ], + ), + ); + } + + Widget _shareItem( + IconData icon, + String title, { + VoidCallback? onTap, + }) { + return InkWell( + onTap: onTap, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 15.w), + margin: EdgeInsets.only(bottom: 10.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.r), + ), + child: Row( + children: [ + Icon( + icon, + color: Colors.black87, + ), + 10.horizontalSpace, + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 14.sp, + color: Colors.black87, + ), + ), + ), + ], + ), + ), + ); + } +} From 565deba7da244b74e7ee92d5a1090c91189ea2af Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Mon, 23 Jun 2025 16:57:22 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E9=9D=A2=E5=AF=B9?= =?UTF-8?q?=E9=9D=A2=E7=BF=BB=E8=AF=91=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96ui?= 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) { From de40cc1dbf079181f5eac62f9ffc7b7982d7b318 Mon Sep 17 00:00:00 2001 From: wolfplus2048 <30993207+wolfplus2048@users.noreply.github.com> Date: Mon, 23 Jun 2025 11:26:43 +0100 Subject: [PATCH 3/3] add --- .../bytedance_speech/BytedanceAudioPlayer.kt | 110 +++++++++++++++++- .../bytedance_speech/BytedanceTTS.kt | 2 +- 2 files changed, 106 insertions(+), 6 deletions(-) diff --git a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt index a761c015c..f1c0ec79a 100644 --- a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt +++ b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceAudioPlayer.kt @@ -15,12 +15,13 @@ import com.deep_voice.speech.tts.AudioDataListener import com.deep_voice.speech.tts.AudioOutputDevice /** - * 高性能音频播放器 - 16kHz采样率 + * 语音通信音频播放器 - 16kHz采样率 * 设计要点: - * • 永远使用 USAGE_MEDIA + MODE_NORMAL 媒体播放通路 + * • 使用 USAGE_VOICE_COMMUNICATION + MODE_IN_COMMUNICATION 通信通路 * • 利用 AudioDeviceCallback 监听设备热插拔 * • API 31+ 使用 setPreferredDevice() 精准路由 * • 有耳机自动从耳机播放,无耳机自动切到扬声器 + * • 内置回声消除和噪音抑制功能 */ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, AudioDeviceCallback() { companion object { @@ -35,6 +36,35 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, Au private var isFirstData = true private var totalBytesWritten = 0 private var sessionActive = true + private var audioFocusRequest: Any? = null // 兼容不同API版本 + + // 音频焦点变化监听器 + private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange -> + when (focusChange) { + AudioManager.AUDIOFOCUS_LOSS -> { + // 永久失去焦点,停止播放 + Log.d(TAG, "音频焦点丢失") + stop() + } + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { + // 暂时失去焦点,暂停播放 + Log.d(TAG, "音频焦点暂时丢失") + audioTrack?.pause() + } + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { + // 暂时失去焦点但可降低音量,这里选择暂停 + Log.d(TAG, "音频焦点暂时丢失(可降低音量)") + audioTrack?.pause() + } + AudioManager.AUDIOFOCUS_GAIN -> { + // 重新获得焦点,恢复播放 + Log.d(TAG, "音频焦点恢复") + if (sessionActive) { + audioTrack?.play() + } + } + } + } // 回调 private var onPlayStarted: (() -> Unit)? = null @@ -62,6 +92,8 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, Au init { // 注册设备热插拔监听 audioManager.registerAudioDeviceCallback(this, Handler(Looper.getMainLooper())) + // 请求音频焦点 + requestAudioFocus() buildTrack(selectOutputDevice()) // 首次创建 } @@ -126,7 +158,7 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, Au audioTrack = AudioTrack.Builder() .setAudioAttributes( AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_MEDIA) // 统一媒体通路 + .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) // 语音通信通路 .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) .build() ) @@ -159,8 +191,8 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, Au track.play() } - // 配置音频管理器 - 统一使用 MODE_NORMAL - audioManager.mode = AudioManager.MODE_NORMAL + // 配置音频管理器 - 使用通信模式以启用回声消除和噪音抑制 + audioManager.mode = AudioManager.MODE_IN_COMMUNICATION // 切换扬声器开关仅在无耳机时打开 audioManager.isSpeakerphoneOn = (device == null) @@ -168,6 +200,71 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, Au Log.d(TAG, "AudioTrack已创建 - 输出设备: $deviceName, 采样率: ${SAMPLE_RATE}Hz") } + /** + * 请求音频焦点 + */ + private fun requestAudioFocus() { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Android O及以上使用新的API + val focusRequest = android.media.AudioFocusRequest.Builder( + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK + ) + .setAudioAttributes( + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) + .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) + .build() + ) + .setAcceptsDelayedFocusGain(true) + .setOnAudioFocusChangeListener(audioFocusChangeListener) + .build() + + val result = audioManager.requestAudioFocus(focusRequest) + if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + audioFocusRequest = focusRequest + Log.d(TAG, "音频焦点请求成功") + } else { + Log.w(TAG, "音频焦点请求失败") + } + } else { + // Android O以下使用旧的API + @Suppress("DEPRECATION") + val result = audioManager.requestAudioFocus( + audioFocusChangeListener, + AudioManager.STREAM_VOICE_CALL, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK + ) + if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + Log.d(TAG, "音频焦点请求成功(旧API)") + } else { + Log.w(TAG, "音频焦点请求失败(旧API)") + } + } + } catch (e: Exception) { + Log.e(TAG, "请求音频焦点异常: ${e.message}") + } + } + + /** + * 释放音频焦点 + */ + private fun releaseAudioFocus() { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + audioFocusRequest?.let { + audioManager.abandonAudioFocusRequest(it as android.media.AudioFocusRequest) + } + } else { + @Suppress("DEPRECATION") + audioManager.abandonAudioFocus(audioFocusChangeListener) + } + Log.d(TAG, "音频焦点已释放") + } catch (e: Exception) { + Log.e(TAG, "释放音频焦点异常: ${e.message}") + } + } + /** * 开始新的播放会话 */ @@ -262,7 +359,10 @@ class BytedanceAudioPlayer(private val context: Context) : AudioDataListener, Au stop() audioTrack?.release() audioTrack = null + releaseAudioFocus() audioManager.unregisterAudioDeviceCallback(this) + // 恢复音频模式 + audioManager.mode = AudioManager.MODE_NORMAL } /** diff --git a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt index bb446a994..9db3c0e93 100644 --- a/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt +++ b/local_plugins/bytedance_speech/android/src/main/kotlin/com/deep_voice/bytedance_speech/BytedanceTTS.kt @@ -116,7 +116,7 @@ class BytedanceTTS(private val context: Context) : ITtsService, CoroutineScope { // TTS配置参数 private var appId: String = "" private var accessToken: String = "" - private var speaker: String = "zh_female_shuangkuaisisi_emo_v2_mars_bigtts" // 默认语音,使用Java示例的音色 + private var speaker: String = "zh_female_yuanqinvyou_moon_bigtts" // 默认语音,使用Java示例的音色 private var format: String = "pcm" // pcm 格式 private var sampleRate: Int = 16000 // 使用16kHz采样率 private var enableTimestamp: Boolean = false