From 9e0017b268f285ecc8432dd3bb34e1cd26253f56 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Mon, 23 Jun 2025 10:55:21 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E4=BF=9D=E5=AE=9A=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/data/models/user_Info.dart | 28 ++-- lib/data/services/network/api.dart | 9 ++ .../controllers/pairing_controller.dart | 129 +++++++++++++----- lib/modules/pairing/views/pairing_view.dart | 51 ++++++- .../pairing/views/pairing_view_new.dart | 73 +++++++--- lib/modules/settings/views/settings_view.dart | 3 +- 6 files changed, 223 insertions(+), 70 deletions(-) diff --git a/lib/data/models/user_Info.dart b/lib/data/models/user_Info.dart index cf5853ee6..63f6d60be 100644 --- a/lib/data/models/user_Info.dart +++ b/lib/data/models/user_Info.dart @@ -14,14 +14,15 @@ class User { String name; String uid; String avatar; - String deviceid; String language; String token; String mail; String phone; String phonemac; String password; - int membershipLevel = 0; //添加会员等级字段,0表示普通用户 + int viplv = 0; //添加会员等级字段,0表示普通用户 + String earphonemac; + String earphoneauthcode; // final Int64 lastbettime; // final Int64 createtime; // final String wxopenid; @@ -33,14 +34,15 @@ class User { required this.name, required this.uid, required this.avatar, - required this.deviceid, required this.language, required this.token, required this.mail, required this.phone, required this.phonemac, required this.password, - this.membershipLevel = 0, // 添加默认值 + required this.viplv, // 添加默认值 + required this.earphonemac, + required this.earphoneauthcode, // required this.lastbettime, // required this.createtime, // required this.wxopenid, @@ -60,14 +62,16 @@ class User { name: (userData?['name'] as String?) ?? '默认名称', // 提供默认值 uid: (userData?['uid'] as String?) ?? '', avatar: (userData?['avatar'] as String?) ?? '', - deviceid: (userData?['deviceid'] as String?) ?? '', + language: (userData?['language'] as String?) ?? 'en', mail: (userData?['mail'] as String?) ?? '', phone: (userData?['phone'] as String?) ?? '', phonemac: (userData?['phonemac'] as String?) ?? '', password: (userData?['password'] as String?) ?? '', - membershipLevel: (userData?['membershipLevel'] as int?) ?? 0, // 提供默认值 + viplv: (userData?['viplv'] as int?) ?? 0, // 提供默认值 + earphonemac: (userData?['earphonemac'] as String?) ?? '', + earphoneauthcode: (userData?['earphoneauthcode'] as String?) ?? '', // lastbettime: _parseTimestamp(userData['lastbettime']), // createtime: _parseTimestamp(userData['createtime']), // wxopenid: (userData?['wxopenid'] as String?) ?? '', @@ -85,13 +89,16 @@ class User { name: (userData?['name'] as String?) ?? '默认名称', // 提供默认值 uid: (userData?['uid'] as String?) ?? '', avatar: (userData?['avatar'] as String?) ?? '', - deviceid: (userData?['deviceid'] as String?) ?? '', language: (userData?['language'] as String?) ?? 'en', mail: (userData?['mail'] as String?) ?? '', phone: (userData?['phone'] as String?) ?? '', phonemac: (userData?['phonemac'] as String?) ?? '', password: (userData?['password'] as String?) ?? '', + + viplv: (userData?['viplv'] as int?) ?? 0, // 提供默认值 + earphonemac: (userData?['earphonemac'] as String?) ?? '', + earphoneauthcode: (userData?['earphoneauthcode'] as String?) ?? '', // lastbettime: _parseTimestamp(userData['lastbettime']), // createtime: _parseTimestamp(userData['createtime']), // wxopenid: (userData?['wxopenid'] as String?) ?? '', @@ -106,14 +113,17 @@ class User { 'name': name, 'uid': uid, 'avatar': avatar, - 'deviceid': deviceid, + 'language': language, 'token': token, 'mail': mail, 'phone': phone, 'phonemac': phonemac, 'password': password, - 'membershipLevel': membershipLevel, + 'viplv': viplv, + 'earphonemac': earphonemac, + 'earphoneauthcode': earphoneauthcode, + // 'lastbettime': lastbettime, // 'createtime': createtime, // 'wxopenid': wxopenid, diff --git a/lib/data/services/network/api.dart b/lib/data/services/network/api.dart index 07e265b4e..1eb3f41a0 100644 --- a/lib/data/services/network/api.dart +++ b/lib/data/services/network/api.dart @@ -126,4 +126,13 @@ class Api { params: params, ); } + + //绑定设备 + static bindearphone([params]) { + return DioManager().request( + NWMethod.post, + '/api/home/user_bindearphone', + params: params, + ); + } } diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index ed62d4ae9..c6fd22ae9 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -1,4 +1,7 @@ import 'package:app_settings/app_settings.dart'; +import 'package:deep_voice/data/models/user_Info.dart'; +import 'package:deep_voice/data/services/network/api.dart'; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'dart:async'; import 'package:permission_handler/permission_handler.dart'; @@ -15,9 +18,10 @@ enum PairingState { checking, // 检查中(蓝牙、权限等) waitingClassic, // 等待经典蓝牙连接 verifying, // 验证设备 + manualbinding, //手动绑定设备 associating, // 正在进行BLE设备关联 - waitingBle, // 等待BLE连接 - completed, // 配对完成 + waitingBle, //等待BLE连接 + completed, //配对完成 error, // 错误状态 } @@ -31,6 +35,7 @@ enum ErrorType { deviceBound, // 设备已被绑定 associationFailed, // 关联失败 bleTimeout, // BLE连接超时 + authnFailed, // 授权失败 networkError, // 网络错误 } @@ -53,6 +58,7 @@ class PairingController extends GetxController { final isLoading = false.obs; final showConnectButton = false.obs; final showRetryButton = false.obs; + final showAutoCodeInput = false.obs; final showSkipButton = false.obs; final connectButtonText = '连接设备'.obs; final retryButtonText = '重试'.obs; @@ -68,6 +74,7 @@ class PairingController extends GetxController { // ========== 当前设备信息 ========== BluetoothDevice? _currentClassicDevice; String? _verifiedDeviceAddress; + TextEditingController codeInputController = TextEditingController(); // Public getter for view access BluetoothDevice? get currentDevice => _currentClassicDevice; @@ -236,6 +243,7 @@ class PairingController extends GetxController { showConnectButton.value = false; showRetryButton.value = false; showSkipButton.value = false; + showAutoCodeInput.value = false; switch (state) { case PairingState.idle: @@ -276,7 +284,12 @@ class PairingController extends GetxController { _startBleConnection(); }); break; - + case PairingState.manualbinding: + statusText.value = '需要手动版定授权码...'; + isLoading.value = false; + showAutoCodeInput.value = true; + showSkipButton.value = true; + break; case PairingState.completed: statusText.value = '配对成功!'; isLoading.value = false; @@ -306,6 +319,10 @@ class PairingController extends GetxController { showRetryButton.value = true; retryButtonText.value = '重新关联'; break; + case ErrorType.authnFailed: + showRetryButton.value = true; + retryButtonText.value = '重新授权'; + break; default: showRetryButton.value = true; retryButtonText.value = '重试'; @@ -393,44 +410,48 @@ class PairingController extends GetxController { Future _verifyDevice(BluetoothDevice device) async { try { Logger.i(_tag, '开始验证设备: ${device.name} (${device.address})'); - // TODO: 实现API验证逻辑 // 暂时模拟验证通过 - await Future.delayed(const Duration(seconds: 2)); - - // 检查设备名称是否包含 DeepSound 或 DeapSound - final deviceName = device.name ?? ''; - if (deviceName.contains('DeepSound') || - deviceName.contains('DeapSound')) { - Logger.i(_tag, '设备验证通过'); - _verifiedDeviceAddress = device.address; - - // 检查是否已有BLE关联 - final pairedMacAddress = await _bleManager.getPairedMacAddress(); - Logger.i(_tag, '已关联的MAC地址: $pairedMacAddress'); - - if (pairedMacAddress != null && - pairedMacAddress.toUpperCase() == device.address.toUpperCase()) { - // 已有关联,检查BLE连接状态 - Logger.i(_tag, '设备已关联,检查BLE连接状态'); - final currentBleState = await _bleManager.getConnectionState(); - Logger.i(_tag, '验证时BLE状态: $currentBleState'); - - if (currentBleState == 2) { - // BleConnectionState.connected - Logger.i(_tag, 'BLE已连接,直接完成配对'); - _transition(PairingState.completed); + // await Future.delayed(const Duration(seconds: 2));; + if (await authbindingDevice()) { + // 检查设备名称是否包含 DeepSound 或 DeapSound + final deviceName = device.name ?? ''; + if (deviceName.contains('DeepSound') || + deviceName.contains('DeapSound')) { + Logger.i(_tag, '设备验证通过'); + _verifiedDeviceAddress = device.address; + + // 检查是否已有BLE关联 + final pairedMacAddress = await _bleManager.getPairedMacAddress(); + Logger.i(_tag, '已关联的MAC地址: $pairedMacAddress'); + + if (pairedMacAddress != null && + pairedMacAddress.toUpperCase() == device.address.toUpperCase()) { + // 已有关联,检查BLE连接状态 + Logger.i(_tag, '设备已关联,检查BLE连接状态'); + final currentBleState = await _bleManager.getConnectionState(); + Logger.i(_tag, '验证时BLE状态: $currentBleState'); + + if (currentBleState == 2) { + // BleConnectionState.connected + Logger.i(_tag, 'BLE已连接,直接完成配对'); + _transition(PairingState.completed); + } else { + Logger.i(_tag, 'BLE未连接,等待BLE连接'); + _transition(PairingState.waitingBle); + } } else { - Logger.i(_tag, 'BLE未连接,等待BLE连接'); - _transition(PairingState.waitingBle); + // 没有关联或关联的是其他设备,需要进行关联 + Logger.i(_tag, '需要进行BLE设备关联'); + _transition(PairingState.associating); } } else { - // 没有关联或关联的是其他设备,需要进行关联 - Logger.i(_tag, '需要进行BLE设备关联'); - _transition(PairingState.associating); + // _setError(ErrorType.deviceNotMatch, '请连接 DeepSound 设备'); + _transition(PairingState.completed); } } else { - _setError(ErrorType.deviceNotMatch, '请连接 DeepSound 设备'); + //验证失败 进入手动授权 + _transition(PairingState.manualbinding); } } catch (e) { Logger.e(_tag, '设备验证异常: $e'); @@ -504,6 +525,46 @@ class PairingController extends GetxController { } } + //自动绑定设备 + Future authbindingDevice() async { + if (User.instance.earphonemac == currentDevice?.address) { + // 已经绑定 + return true; + } + + // 调用BLE管理器的自动绑定方法 + final response = await Api.bindearphone({ + "earphonemac": currentDevice?.address, + "earphoneauthcode": "", + }); + Logger.i(_tag, '手动授权结果 ${response}'); + if (response != null) { + Logger.i(_tag, '设备自动绑定成功'); + User.instance.earphonemac = currentDevice?.address ?? ''; + return true; + } else { + Logger.e(_tag, '手动绑定失败!'); + return false; + } + } + + /// 用户点击授权码输入框 + void onAutoCodeInputTap() async { + // 调用BLE管理器的自动绑定方法 + final response = await Api.bindearphone({ + "earphonemac": currentDevice?.address, + "earphoneauthcode": codeInputController.text.trim(), + }); + Logger.i(_tag, '手动授权结果 ${response}'); + if (response != null) { + _transition(PairingState.verifying); + _verifyDevice(_currentClassicDevice!); + } else { + Logger.e(_tag, '手动绑定失败!'); + _setError(ErrorType.authnFailed, '设备授权失败,请检查授权码'); + } + } + /// 导航到主页 void _navigateToHome() async { await BleManager.to.startAgentService(); diff --git a/lib/modules/pairing/views/pairing_view.dart b/lib/modules/pairing/views/pairing_view.dart index b0cf500c3..542e22ec9 100644 --- a/lib/modules/pairing/views/pairing_view.dart +++ b/lib/modules/pairing/views/pairing_view.dart @@ -336,6 +336,8 @@ class PairingView extends GetView { return '扫描超时,请确保设备在附近'; case ErrorType.networkError: return '网络错误,请检查网络连接'; + case ErrorType.authnFailed: + return '授权失败,请检查授权码是否正确'; default: return '配对失败,请重试'; } @@ -433,7 +435,7 @@ class PairingView extends GetView { final showConnectButton = controller.showConnectButton.value; final showRetryButton = controller.showRetryButton.value; final showSkipButton = controller.showSkipButton.value; - + final showAutoCodeInput = controller.showAutoCodeInput.value; return Container( width: double.infinity, child: Column( @@ -490,6 +492,51 @@ class PairingView extends GetView { ), ), ), + if (showAutoCodeInput) + Column( + children: [ + // 输入框 + SizedBox( + width: double.infinity, + height: 48.h, + child: TextField( + controller: controller + .codeInputController, // 你可以在 Controller 中声明这个 + decoration: InputDecoration( + hintText: '请输入代码', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12.r), + ), + contentPadding: EdgeInsets.symmetric(horizontal: 16.w), + ), + ), + ), + SizedBox(height: 12.h), + SizedBox( + width: double.infinity, + height: 48.h, + child: ElevatedButton( + onPressed: controller.onAutoCodeInputTap, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(Get.context!).primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + elevation: 0, + ), + child: Text( + '授权绑定', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ), + ], + ), + // 自动输入代码按钮 // 跳过按钮 if (showSkipButton) ...[ @@ -510,11 +557,9 @@ class PairingView extends GetView { ), ), ], - ], ), ); }); } - } diff --git a/lib/modules/pairing/views/pairing_view_new.dart b/lib/modules/pairing/views/pairing_view_new.dart index eae654a1b..d4b843744 100644 --- a/lib/modules/pairing/views/pairing_view_new.dart +++ b/lib/modules/pairing/views/pairing_view_new.dart @@ -21,17 +21,17 @@ class PairingView extends GetView { children: [ // 顶部标题 _buildHeader(isDarkMode), - + // 中间内容 Expanded( child: Center( child: Obx(() => _buildContent(isDarkMode)), ), ), - + // 底部按钮 Obx(() => _buildButtons(isDarkMode)), - + SizedBox(height: 20.h), ], ), @@ -69,15 +69,15 @@ class PairingView extends GetView { Widget _buildContent(bool isDarkMode) { final state = controller.currentState; final isLoading = controller.isLoading.value; - + return Column( mainAxisSize: MainAxisSize.min, children: [ // 状态图标 _buildStateIcon(state, isLoading, isDarkMode), - + SizedBox(height: 40.h), - + // 状态文本 Text( controller.statusText.value, @@ -87,7 +87,7 @@ class PairingView extends GetView { ), textAlign: TextAlign.center, ), - + // 错误详情 if (state == PairingState.error) ...[ SizedBox(height: 16.h), @@ -132,48 +132,51 @@ class PairingView extends GetView { ), ); } - + IconData iconData; Color iconColor; - + switch (state) { case PairingState.idle: case PairingState.checking: iconData = Icons.bluetooth_searching; iconColor = Colors.blue; break; - + case PairingState.waitingClassic: iconData = Icons.bluetooth; iconColor = Colors.grey; break; - + case PairingState.verifying: iconData = Icons.verified_user; iconColor = Colors.orange; break; - + case PairingState.associating: iconData = Icons.link; iconColor = Colors.blue; break; - + case PairingState.waitingBle: iconData = Icons.sync; iconColor = Colors.blue; break; - + case PairingState.manualbinding: + iconData = Icons.blind; + iconColor = Colors.blue; + break; case PairingState.completed: iconData = Icons.check_circle; iconColor = Colors.green; break; - + case PairingState.error: iconData = Icons.error; iconColor = Colors.red; break; } - + return Container( width: 100.w, height: 100.w, @@ -193,8 +196,9 @@ class PairingView extends GetView { Widget _buildButtons(bool isDarkMode) { final showConnectButton = controller.showConnectButton.value; final showRetryButton = controller.showRetryButton.value; + final showAutoCodeInput = controller.showAutoCodeInput.value; final showSkipButton = controller.showSkipButton.value; - + return Column( children: [ // 主要操作按钮 @@ -204,16 +208,40 @@ class PairingView extends GetView { controller.onConnectTap, isDarkMode, ), - + if (showRetryButton) _buildPrimaryButton( '重试', controller.onRetryTap, isDarkMode, ), - + SizedBox(height: 12.h), - + if (showAutoCodeInput) + // 自动输入代码按钮 + SizedBox( + width: double.infinity, + height: 48.h, + child: ElevatedButton( + onPressed: controller.onAutoCodeInputTap, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(Get.context!).primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + elevation: 0, + ), + child: Text( + '自动输入代码', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ), + // 跳过按钮 if (showSkipButton) TextButton( @@ -231,7 +259,8 @@ class PairingView extends GetView { } /// 构建主要按钮 - Widget _buildPrimaryButton(String text, VoidCallback onPressed, bool isDarkMode) { + Widget _buildPrimaryButton( + String text, VoidCallback onPressed, bool isDarkMode) { return SizedBox( width: double.infinity, height: 48.h, @@ -255,4 +284,4 @@ class PairingView extends GetView { ), ); } -} \ No newline at end of file +} diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index 35bb6c90b..4f16e2b33 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -200,8 +200,7 @@ class SettingsView extends GetView { 2: 'proUser', //'专业用户' }; final membershipString = membershipMap[ - controller - .userInfo.value.membershipLevel] ?? + controller.userInfo.value.viplv] ?? 'Unknown'; return Text( membershipString, From 759c2d3ed6174619cc44188e56e81e6686923aa0 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Mon, 23 Jun 2025 11:24:55 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=BC=BA=E5=88=B6?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E6=89=AC=E5=A3=B0=E5=99=A8=E4=B8=8D=E7=94=9F?= =?UTF-8?q?=E6=95=88=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../speech_impl/azure_tts_service.dart | 14 ++++++- .../speech_impl/voice_clone_tts_service.dart | 8 +++- lib/data/services/tts_service.dart | 39 +++++++++---------- .../azure_speech/AzureSpeechPlugin.kt | 6 ++- .../azure_speech/AzureTtsHelper.kt | 6 ++- 5 files changed, 49 insertions(+), 24 deletions(-) diff --git a/lib/data/services/speech_impl/azure_tts_service.dart b/lib/data/services/speech_impl/azure_tts_service.dart index d6eccdf9b..8b2512260 100644 --- a/lib/data/services/speech_impl/azure_tts_service.dart +++ b/lib/data/services/speech_impl/azure_tts_service.dart @@ -413,4 +413,16 @@ class AzureTtsService extends GetxService implements TtsService { Logger.error('释放资源失败: ${e.toString()}'); } } -} \ No newline at end of file + + @override + Future setAudioOutputDevice() async { + try { + // 开始合成,传递参数 + final result = await _channel.invokeMethod('setAudioOutputDevice'); + return result; + } catch (e) { + Logger.error('音频输出设备类型失败: ${e.toString()}'); + return false; + } + } +} 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 43079325f..656fe4bcb 100644 --- a/lib/data/services/speech_impl/voice_clone_tts_service.dart +++ b/lib/data/services/speech_impl/voice_clone_tts_service.dart @@ -390,4 +390,10 @@ class VoiceCloneTtsService extends GetxService implements TtsService { Logger.error('释放资源失败: ${e.toString()}'); } } -} \ No newline at end of file + + @override + Future setAudioOutputDevice() { + // TODO: implement setAudioOutputDevice + throw UnimplementedError(); + } +} diff --git a/lib/data/services/tts_service.dart b/lib/data/services/tts_service.dart index 62d938b56..4e2ec3ef4 100644 --- a/lib/data/services/tts_service.dart +++ b/lib/data/services/tts_service.dart @@ -2,52 +2,51 @@ import 'dart:async'; /// 语音合成服务接口 abstract class TtsService { - /// 当前选择的语音 String get currentVoice; - + /// 当前是否正在播放 bool get isSpeaking; - + /// TTS事件流,用于监听语音合成事件 Stream get onEvent; - + /// 初始化语音合成服务 Future initialize({ List? supportedLanguages, }); - + /// 设置语音 Future setVoice(String voiceName); - - + /// 单次播放文本,完成后返回 Future speakOnce(String text); - + /// 将文本加入播放队列,支持连续播放多段文本 Future speakStream(String text); - + /// 播放队列中剩余文本 Future flushStream(); - + /// 停止播放 Future stop(); - + /// 释放资源 Future dispose(); + Future setAudioOutputDevice(); } /// TTS事件类型 enum TtsEventType { /// 开始播放 started, - + /// 播放完成 completed, - + /// 播放取消 canceled, - + /// 播放错误 error, } @@ -56,15 +55,15 @@ enum TtsEventType { class TtsEvent { /// 事件类型 final TtsEventType type; - + /// 错误信息(仅在 error 类型中有效) final String error; - + TtsEvent({ required this.type, this.error = '', }); - + /// 创建错误事件的快捷构造函数 factory TtsEvent.error(String errorMessage) { return TtsEvent( @@ -72,12 +71,12 @@ class TtsEvent { error: errorMessage, ); } - + /// 检查是否为错误 bool get isError => type == TtsEventType.error; - + @override String toString() { return 'TtsEvent{type: $type, error: $error}'; } -} \ No newline at end of file +} 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 54e960092..498613b8c 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 @@ -15,7 +15,7 @@ import com.deep_voice.speech.tts.TtsEvent import com.deep_voice.speech.tts.TtsEventListener import com.deep_voice.speech.tts.TtsEventType import com.yunqiinnovation.ble_service.BleService - +import com.deep_voice.speech.tts.AudioOutputDevice /** AzureSpeechPlugin */ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { private val tag = "AzureSpeechPlugin" @@ -501,6 +501,10 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { azureTtsHelper.release() result.success(true) } + "setAudioOutputDevice" -> { + azureTtsHelper.setAudioOutputDevice(AudioOutputDevice.SPEAKER) + result.success(true) + } else -> { result.notImplemented() 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 ea9d42588..d5444ffb2 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 @@ -223,7 +223,11 @@ class AzureTtsHelper(private val context: Context) : ITtsService { } AudioOutputDevice.SPEAKER -> { // 强制使用扬声器 - manager.mode = AudioManager.MODE_NORMAL + /* MODE_NORMAL 默认媒体模式 ❌ 不能强制扬声器(会被耳机覆盖) + MODE_IN_COMMUNICATION VoIP 通信模式 ✅ 可以强制使用扬声器或听筒 + MODE_IN_CALL 电话模式(仅限系统) ✅ 更强但风险大,不推荐使用 + */ + manager.mode = AudioManager.MODE_IN_COMMUNICATION manager.isSpeakerphoneOn = true } AudioOutputDevice.HEADPHONES -> { From c7f6161ef77a9e2011f6fb7169613d6c06dc8f02 Mon Sep 17 00:00:00 2001 From: tanlongsheng <252620078@qq.com> Date: Mon, 23 Jun 2025 13:03:56 +0800 Subject: [PATCH 3/4] =?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 c15894fb605ed130874f9b5fdba1e398d2ed8e27 Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Mon, 23 Jun 2025 16:57:22 +0800 Subject: [PATCH 4/4] =?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) {