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/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/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index f1aa28475..1915aaba3 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() { Future.delayed(const Duration(seconds: 1), () async { 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 777f8c48f..4cc3e7360 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, 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 -> {