Browse Source

Merge branch 'new_dev' of github.com:deepcloud2048/deep_voice into new_dev

newdev_shunjiawei
wolfplus2048 1 year ago
parent
commit
b2b70dd935
  1. 33
      lib/core/utils/synchrodata.dart
  2. 28
      lib/data/models/user_Info.dart
  3. 9
      lib/data/services/network/api.dart
  4. 16
      lib/data/services/speech_impl/azure_tts_service.dart
  5. 8
      lib/data/services/speech_impl/voice_clone_tts_service.dart
  6. 41
      lib/data/services/tts_service.dart
  7. 62
      lib/modules/explore/views/explore_view.dart
  8. 101
      lib/modules/meeting/views/generate_bottom_sheet.dart
  9. 14
      lib/modules/meeting/views/meeting_details_view.dart
  10. 1075
      lib/modules/meeting/views/meeting_view.dart
  11. 92
      lib/modules/meeting/views/more_bottom_sheet.dart
  12. 129
      lib/modules/pairing/controllers/pairing_controller.dart
  13. 51
      lib/modules/pairing/views/pairing_view.dart
  14. 73
      lib/modules/pairing/views/pairing_view_new.dart
  15. 3
      lib/modules/settings/views/settings_view.dart
  16. 85
      lib/modules/translation/controllers/translation_controller.dart
  17. 20
      lib/modules/translation/views/translation_list_view.dart
  18. 8
      lib/modules/translation/views/translation_view.dart
  19. 13
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  20. 18
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  21. 7
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt

33
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<Map<String, dynamic>> _records =
(_storage.read("operation_records") as List?)
?.map((e) => Map<String, dynamic>.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<String, dynamic>.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<String, dynamic>? foundElement = _records.firstWhere(
(element) => element['id'] == id,
orElse: () => null,
orElse: () => {},
);
if (foundElement != null) {
if (foundElement.isNotEmpty) {
foundElement.addAll({
...params,
...Map<String, dynamic>.from(params),
'id': id,
'number': params['id'],
});
} else {
_records.add({
...params,
...Map<String, dynamic>.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<String, dynamic>.from(result),
...Map<String, dynamic>.from(result['details']),
'rtype': result['type'],
'original': jsonEncode(speakerList),
...element,
...Map<String, dynamic>.from(element),
});
}
var result = await Api.putOperationRecord({

28
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,

9
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,
);
}
}

16
lib/data/services/speech_impl/azure_tts_service.dart

@ -413,4 +413,18 @@ class AzureTtsService extends GetxService implements TtsService {
Logger.error('释放资源失败: ${e.toString()}');
}
}
}
@override
Future<bool> setAudioOutputDevice(int type) async {
try {
// 开始合成,传递参数
final result = await _channel.invokeMethod('setAudioOutputDevice', {
'type': type,
});
return result;
} catch (e) {
Logger.error('音频输出设备类型失败: ${e.toString()}');
return false;
}
}
}

8
lib/data/services/speech_impl/voice_clone_tts_service.dart

@ -390,4 +390,10 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
Logger.error('释放资源失败: ${e.toString()}');
}
}
}
@override
Future<bool> setAudioOutputDevice(int type) {
// TODO: implement setAudioOutputDevice
throw UnimplementedError();
}
}

41
lib/data/services/tts_service.dart

@ -2,52 +2,53 @@ import 'dart:async';
/// 语音合成服务接口
abstract class TtsService {
/// 当前选择的语音
String get currentVoice;
/// 当前是否正在播放
bool get isSpeaking;
/// TTS事件流,用于监听语音合成事件
Stream<TtsEvent> get onEvent;
/// 初始化语音合成服务
Future<bool> initialize({
List<String>? supportedLanguages,
});
/// 设置语音
Future<bool> setVoice(String voiceName);
/// 单次播放文本,完成后返回
Future<bool> speakOnce(String text);
/// 将文本加入播放队列,支持连续播放多段文本
Future<bool> speakStream(String text);
/// 播放队列中剩余文本
Future<bool> flushStream();
/// 停止播放
Future<bool> stop();
/// 释放资源
Future<void> dispose();
/// 切换音频输出设备
Future<bool> setAudioOutputDevice(int type);
}
/// TTS事件类型
enum TtsEventType {
/// 开始播放
started,
/// 播放完成
completed,
/// 播放取消
canceled,
/// 播放错误
error,
}
@ -56,15 +57,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 +73,12 @@ class TtsEvent {
error: errorMessage,
);
}
/// 检查是否为错误
bool get isError => type == TtsEventType.error;
@override
String toString() {
return 'TtsEvent{type: $type, error: $error}';
}
}
}

62
lib/modules/explore/views/explore_view.dart

@ -126,37 +126,37 @@ class ExploreView extends GetView<ExploreController> {
],
),
// 搜索框
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,
// ),
// ),
// ),
],
),
);

101
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<GenerateBottomSheet> createState() => _GenerateBottomSheetState();
@ -214,56 +216,57 @@ class _GenerateBottomSheetState extends State<GenerateBottomSheet> {
},
),
),
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<GenerateBottomSheet> {
_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(

14
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<MeetingDetailsController> {
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),
),
),
);
},
),
],
);

1075
lib/modules/meeting/views/meeting_view.dart

File diff suppressed because it is too large

92
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<MoreBottomSheet> createState() => _MoreBottomSheetState();
}
class _MoreBottomSheetState extends State<MoreBottomSheet> {
final _controller = Get.find<MeetingController>();
final _detailsController = Get.find<MeetingDetailsController>();
@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,
),
),
),
],
),
),
);
}
}

129
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<void> _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<bool> 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 {

51
lib/modules/pairing/views/pairing_view.dart

@ -336,6 +336,8 @@ class PairingView extends GetView<PairingController> {
return '扫描超时,请确保设备在附近';
case ErrorType.networkError:
return '网络错误,请检查网络连接';
case ErrorType.authnFailed:
return '授权失败,请检查授权码是否正确';
default:
return '配对失败,请重试';
}
@ -433,7 +435,7 @@ class PairingView extends GetView<PairingController> {
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<PairingController> {
),
),
),
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<PairingController> {
),
),
],
],
),
);
});
}
}

73
lib/modules/pairing/views/pairing_view_new.dart

@ -21,17 +21,17 @@ class PairingView extends GetView<PairingController> {
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<PairingController> {
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<PairingController> {
),
textAlign: TextAlign.center,
),
// 错误详情
if (state == PairingState.error) ...[
SizedBox(height: 16.h),
@ -132,48 +132,51 @@ class PairingView extends GetView<PairingController> {
),
);
}
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<PairingController> {
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<PairingController> {
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<PairingController> {
}
/// 构建主要按钮
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<PairingController> {
),
);
}
}
}

3
lib/modules/settings/views/settings_view.dart

@ -200,8 +200,7 @@ class SettingsView extends GetView<SettingsController> {
2: 'proUser', //'专业用户'
};
final membershipString = membershipMap[
controller
.userInfo.value.membershipLevel] ??
controller.userInfo.value.viplv] ??
'Unknown';
return Text(
membershipString,

85
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<AsrService>();
@ -55,6 +62,7 @@ class TranslationController extends GetxController {
final appDocDir = getApplicationDocumentsDirectory();
var appDir;
var dir;
Future<void> 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<bool> _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<void> 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(

20
lib/modules/translation/views/translation_list_view.dart

@ -278,15 +278,16 @@ class TranslationListView extends GetView<TranslationListController> {
),
_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<TranslationListController> {
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(); // 刷新列表
},
),
],

8
lib/modules/translation/views/translation_view.dart

@ -110,7 +110,9 @@ class TranslationView extends GetView<TranslationController> {
// 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<TranslationController> {
// 历史记录按钮
IconButton(
icon: Icon(
Icons.history,
Icons.folder,
color: isDarkMode ? Colors.white : Colors.black,
size: 24.sp,
),
@ -134,7 +136,7 @@ class TranslationView extends GetView<TranslationController> {
// 返回时不自动开始翻译,只需重新设置滚动监听器
controller.setupScrollListener();
},
tooltip: '查看历史记录',
tooltip: '查看录音文件',
),
],
bottom: PreferredSize(

13
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

18
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,22 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin {
azureTtsHelper.release()
result.success(true)
}
"setAudioOutputDevice" -> {
val type = call.argument<Int>("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)
}
else -> {
result.notImplemented()

7
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) {
@ -223,7 +224,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 -> {

Loading…
Cancel
Save