Browse Source

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

newdev_shunjiawei
lxm 11 months ago
parent
commit
1dad936c2d
  1. 2
      lib/data/models/appconfig_model.dart
  2. 2
      lib/data/models/appconfig_model.g.dart
  3. 18
      lib/data/services/ota_check_service.dart
  4. 2
      lib/modules/home/controllers/home_controller.dart
  5. 13
      lib/modules/ota_upgrade/controllers/ota_core_contropller.dart
  6. 7
      lib/modules/ota_upgrade/views/ota_upgrade_view.dart
  7. 2
      lib/modules/pairing/controllers/pairing_controller.dart
  8. 125
      lib/modules/translation/controllers/translation_controller.dart
  9. 34
      lib/modules/translation/controllers/translation_history_controller.dart
  10. 25
      lib/modules/translation/views/translation_view.dart
  11. 130
      lib/modules/translation/views/widgets/face_to_face_layout.dart
  12. 10
      lib/modules/translation/views/widgets/translation_item_widget.dart
  13. 10
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt
  14. 2
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  15. 51
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift

2
lib/data/models/appconfig_model.dart

@ -79,6 +79,7 @@ class DBProduct {
final String? productimage; final String? productimage;
final String scanuuid; final String scanuuid;
final String? version; final String? version;
bool? isforceupdate;
final bool? iscontinuouschat; final bool? iscontinuouschat;
final String? updatedescription; final String? updatedescription;
final String? updatepackageaddress; final String? updatepackageaddress;
@ -91,6 +92,7 @@ class DBProduct {
required this.productimage, required this.productimage,
required this.scanuuid, required this.scanuuid,
required this.version, required this.version,
required this.isforceupdate, //该产品固件版本是否强制更新
required this.iscontinuouschat, required this.iscontinuouschat,
required this.updatedescription, required this.updatedescription,
required this.updatepackageaddress, required this.updatepackageaddress,

2
lib/data/models/appconfig_model.g.dart

@ -79,6 +79,7 @@ DBProduct _$DBProductFromJson(Map<String, dynamic> json) => DBProduct(
productimage: json['productimage'] as String?, productimage: json['productimage'] as String?,
scanuuid: json['scanuuid'] as String, scanuuid: json['scanuuid'] as String,
version: json['version'] as String?, version: json['version'] as String?,
isforceupdate: json['isforceupdate'] as bool?,
iscontinuouschat: json['iscontinuouschat'] as bool?, iscontinuouschat: json['iscontinuouschat'] as bool?,
updatedescription: json['updatedescription'] as String?, updatedescription: json['updatedescription'] as String?,
updatepackageaddress: json['updatepackageaddress'] as String?, updatepackageaddress: json['updatepackageaddress'] as String?,
@ -91,6 +92,7 @@ Map<String, dynamic> _$DBProductToJson(DBProduct instance) => <String, dynamic>{
'productimage': instance.productimage, 'productimage': instance.productimage,
'scanuuid': instance.scanuuid, 'scanuuid': instance.scanuuid,
'version': instance.version, 'version': instance.version,
'isforceupdate': instance.isforceupdate,
'iscontinuouschat': instance.iscontinuouschat, 'iscontinuouschat': instance.iscontinuouschat,
'devicename': instance.devicename, 'devicename': instance.devicename,
'updatedescription': instance.updatedescription, 'updatedescription': instance.updatedescription,

18
lib/data/services/ota_check_service.dart

@ -57,6 +57,7 @@ class OtaCheckService extends GetxService {
/// [isFullyConnected] 设备是否完全连接 /// [isFullyConnected] 设备是否完全连接
/// 返回是否成功执行了检查 /// 返回是否成功执行了检查
Future<bool> performOtaCheck(bool isFullyConnected) async { Future<bool> performOtaCheck(bool isFullyConnected) async {
bool isForceUpdate = false;
try { try {
// 检查是否应该进行OTA检查 // 检查是否应该进行OTA检查
if (!shouldCheckOta()) { if (!shouldCheckOta()) {
@ -95,7 +96,12 @@ class OtaCheckService extends GetxService {
_otaCoreService.isNewVersionAvailable(currentVersion, latestVersion); _otaCoreService.isNewVersionAvailable(currentVersion, latestVersion);
if (hasUpdate) { if (hasUpdate) {
Logger.d(_tag, '发现新版本,准备显示升级提醒'); isForceUpdate = latestInfo['isForceUpdate'] as bool? ?? false;
if (isForceUpdate) {
Logger.d(_tag, '发现强制更新版本,准备显示升级提醒');
} else {
Logger.d(_tag, '发现新版本,准备显示升级提醒');
}
// 再次确认当前在Home页面(防止异步执行期间页面切换) // 再次确认当前在Home页面(防止异步执行期间页面切换)
if (_isCurrentlyOnHomePage()) { if (_isCurrentlyOnHomePage()) {
@ -109,12 +115,18 @@ class OtaCheckService extends GetxService {
} }
// 标记为已检查(无论是否有更新都标记为已检查) // 标记为已检查(无论是否有更新都标记为已检查)
_markAsChecked(); if (!isForceUpdate) {
_markAsChecked();
}
_isCheckingInProgress = false;
return true; return true;
} catch (e) { } catch (e) {
Logger.e(_tag, '检查OTA升级失败: $e'); Logger.e(_tag, '检查OTA升级失败: $e');
// 即使检查失败,也标记为已检查,避免重复尝试 // 即使检查失败,也标记为已检查,避免重复尝试
_markAsChecked(); if (!isForceUpdate) {
_markAsChecked();
}
_isCheckingInProgress = false;
return false; return false;
} }
} }

2
lib/modules/home/controllers/home_controller.dart

@ -277,7 +277,7 @@ class HomeController extends GetxController {
// 连接成功后检查OTA升级 // 连接成功后检查OTA升级
if (noviceGuideService.isHomeGuide) { if (noviceGuideService.isHomeGuide) {
Future.delayed(const Duration(seconds: 2), () { Future.delayed(const Duration(seconds: 1), () {
_checkOtaUpgrade(); _checkOtaUpgrade();
}); });
} }

13
lib/modules/ota_upgrade/controllers/ota_core_contropller.dart

@ -525,17 +525,20 @@ class OtaCoreService {
} }
} }
product.isforceupdate = product.version?.contains("+");
return { return {
'latestVersion': product.version ?? "", 'latestVersion': product.version?.split('+').first ?? "",
'updateDescription': product.updatedescription ?? "", 'updateDescription': product.updatedescription ?? "",
'updatePackageAddress': product.updatepackageaddress ?? "", 'updatePackageAddress': product.updatepackageaddress ?? "",
'isForceUpdate': product.isforceupdate ?? false,
}; };
} }
/// 比较版本号,判断是否有新版本可用 /// 比较版本号,判断是否有新版本可用
/// [currentVersion] 当前版本号 /// [currentVersion] 当前版本号
/// [latestVersion] 最新版本号 /// [latestVersion] 最新版本号
/// 返回true表示有新版本可用(支持升级和降级) /// 返回true表示有新版本可用(只支持升级)
bool isNewVersionAvailable(String currentVersion, String latestVersion) { bool isNewVersionAvailable(String currentVersion, String latestVersion) {
try { try {
List<int> currentParts = List<int> currentParts =
@ -555,10 +558,12 @@ class OtaCoreService {
latestParts.add(0); latestParts.add(0);
} }
// 比较每一段,只要有不同就返回true(支持升级和降级) // 从高位到低位比较版本号
for (int i = 0; i < maxLength; i++) { for (int i = 0; i < maxLength; i++) {
if (latestParts[i] != currentParts[i]) { if (latestParts[i] > currentParts[i]) {
return true; return true;
} else if (latestParts[i] < currentParts[i]) {
return false;
} }
} }

7
lib/modules/ota_upgrade/views/ota_upgrade_view.dart

@ -13,7 +13,7 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
return WillPopScope( return WillPopScope(
onWillPop: () async { onWillPop: () async {
if (controller.isUpgrade.value) { if (controller.isUpgrade.value && controller.showDetailedStatus.value) {
// 显示确认弹窗 // 显示确认弹窗
await Get.dialog( await Get.dialog(
AlertDialog( AlertDialog(
@ -54,7 +54,8 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
color: isDarkMode ? Colors.white : Colors.black, color: isDarkMode ? Colors.white : Colors.black,
), ),
onPressed: () { onPressed: () {
if (controller.isUpgrade.value) { if (controller.isUpgrade.value &&
controller.showDetailedStatus.value) {
// 显示确认弹窗 // 显示确认弹窗
Get.dialog( Get.dialog(
AlertDialog( AlertDialog(
@ -827,7 +828,7 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
// 更新按钮 // 更新按钮
Widget _buildUpdateButton(bool isDarkMode) { Widget _buildUpdateButton(bool isDarkMode) {
return Obx(() { return Obx(() {
if (controller.isUpgrade.value) { if (controller.isUpgrade.value && controller.showDetailedStatus.value) {
return _buildProgressIndicator(isDarkMode); return _buildProgressIndicator(isDarkMode);
} }

2
lib/modules/pairing/controllers/pairing_controller.dart

@ -512,7 +512,7 @@ class PairingController extends GetxController with WidgetsBindingObserver {
/// 开始初始检查 /// 开始初始检查
void _startInitialCheck() { void _startInitialCheck() {
_transition(PairingState.checking); // _transition(PairingState.checking);
_checkCurrentStatus(); _checkCurrentStatus();
} }

125
lib/modules/translation/controllers/translation_controller.dart

@ -234,6 +234,8 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
Future<void> changeTranslationMode(String mode) async { Future<void> changeTranslationMode(String mode) async {
currentMode.value = mode; currentMode.value = mode;
currentModeTitle.value = _getModeTitle(mode); currentModeTitle.value = _getModeTitle(mode);
_historyManager.setMode(mode);
loadTranslationHistory();
//stopAll(); //stopAll();
// 检查悬浮窗支持情况 // 检查悬浮窗支持情况
@ -397,7 +399,11 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
targetLanguageCode.value = targetAsrCode; targetLanguageCode.value = targetAsrCode;
} }
// 加载历史记录 // 初始化翻译模式(用于历史隔离的初始加载)
final initMode = Get.arguments?['translationId'] ?? 'simultaneous';
_historyManager.setMode(initMode);
// 按初始模式加载历史
loadTranslationHistory(); loadTranslationHistory();
// 初始化服务 // 初始化服务
@ -406,9 +412,8 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
// 初始化历史管理器 // 初始化历史管理器
_historyManager.initialize(); _historyManager.initialize();
// 初始化翻译模式,根据跳转参数 // 应用模式逻辑(含ASR/TTS配置等)
final mode = Get.arguments?['translationId'] ?? 'simultaneous'; changeTranslationMode(initMode);
changeTranslationMode(mode);
// 监听应用生命周期:当应用切到后台/失焦时兜底重置 activeSpeaker // 监听应用生命周期:当应用切到后台/失焦时兜底重置 activeSpeaker
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
@ -847,53 +852,64 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
/// 处理语音识别事件 /// 处理语音识别事件
/// [event] 识别事件 /// [event] 识别事件
void _handleRecognitionEvent(RecognitionEvent event) { void _handleRecognitionEvent(RecognitionEvent event) {
if (event.type == RecognitionEventType.finalResult && // Logger.info("处理识别事件:${event}");
event.text.isNotEmpty) { switch (event.type) {
detectedLanguageCode = event.detectedLanguage; case RecognitionEventType.finalResult:
print("处理识别完成:${event.text}"); if (event.text.isEmpty) break;
if (currentMode.value != 'faceToFace') { // 通话模式下:目标语言即为识别语言,无需判断
handleFinalResult(event.text); detectedLanguageCode = currentMode.value == 'call'
} else { ? targetLanguageCode.value
faceToFaceRecognitionText.value += event.text; : event.detectedLanguage;
// 添加日志打印 Logger.info("处理识别完成:${event.text}");
Logger.info('lxm-面对面翻译识别文本更新: ${faceToFaceRecognitionText.value}'); if (currentMode.value != 'faceToFace') {
} handleFinalResult(event.text);
if (currentMode.value == 'faceToFace' && activeSpeaker.value == 0) { } else {
handleFinalResult(faceToFaceRecognitionText.value); faceToFaceRecognitionText.value += event.text;
faceToFaceRecognitionText.value = ''; Logger.info('lxm-面对面翻译识别文本更新: ${faceToFaceRecognitionText.value}');
} }
} else if (event.type == RecognitionEventType.finalResult1 && if (currentMode.value == 'faceToFace' && activeSpeaker.value == 0) {
event.text.isNotEmpty) { handleFinalResult(faceToFaceRecognitionText.value);
// 通话模式下:己方(麦克风)识别完成事件 faceToFaceRecognitionText.value = '';
// 若原生未返回检测语言,则兜底为源语言 }
detectedLanguageCode = event.detectedLanguage.isNotEmpty break;
? event.detectedLanguage
: sourceLanguageCode.value; case RecognitionEventType.finalResult1:
print("处理己方识别完成:${event.text}"); // 通话模式下:己方(麦克风)识别完成事件(对应ast服务)
if (currentMode.value != 'faceToFace') { if (event.text.isEmpty) break;
//通话模式下:己方语言即为源语言,无需判断
detectedLanguageCode = sourceLanguageCode.value;
Logger.info("处理己方识别完成:${event.text}");
handleFinalResult(event.text); handleFinalResult(event.text);
} else { break;
faceToFaceRecognitionText.value += event.text;
Logger.info('lxm-面对面翻译识别文本更新(己方): ${faceToFaceRecognitionText.value}'); case RecognitionEventType.intermediateResult:
} if (event.text.isEmpty) break;
if (currentMode.value == 'faceToFace' && activeSpeaker.value == 0) { // 通话模式下:目标语言即为识别语言,无需判断
handleFinalResult(faceToFaceRecognitionText.value); detectedLanguageCode = currentMode.value == 'call'
faceToFaceRecognitionText.value = ''; ? targetLanguageCode.value
} : event.detectedLanguage;
} else if (event.type == RecognitionEventType.intermediateResult && Logger.info("处理识别中事件:${event.text}");
event.text.isNotEmpty) { handleIntermediateResult(event.text);
if (event.detectedLanguage.isNotEmpty) { break;
detectedLanguageCode = event.detectedLanguage;
} case RecognitionEventType.error:
print("处理识别中事件:${event.text}"); Get.snackbar('error'.tr, event.error);
handleIntermediateResult(event.text); isRecognizing.value = false;
} else if (event.type == RecognitionEventType.error) { Logger.error('语音识别错误: ${event.error}');
Get.snackbar('error'.tr, event.error); break;
isRecognizing.value = false;
Logger.error('语音识别错误: ${event.error}'); case RecognitionEventType.canceled:
} else if (event.type == RecognitionEventType.canceled) { isRecognizing.value = false;
isRecognizing.value = false; Logger.error('语音识别取消: ${event.error}');
Logger.error('语音识别取消: ${event.error}'); break;
case RecognitionEventType.sessionStarted:
// 可按需处理会话开始事件
break;
case RecognitionEventType.sessionStopped:
// 可按需处理会话结束事件
break;
} }
} }
@ -919,18 +935,21 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
/// [text] 识别的文本 /// [text] 识别的文本
void handleIntermediateResult(String text) { void handleIntermediateResult(String text) {
if (text.isEmpty) return; if (text.isEmpty) return;
print( Logger.info(
"handleIntermediateResult:${translationHistory.isEmpty}${translationHistory.isNotEmpty ? translationHistory.last.isIntermediate : 'N/A'}"); "handleIntermediateResult:${translationHistory.isEmpty}${translationHistory.isNotEmpty ? translationHistory.last.isIntermediate : 'N/A'}");
if (translationHistory.isEmpty || if (translationHistory.isEmpty ||
(translationHistory.isNotEmpty && (translationHistory.isNotEmpty &&
!translationHistory.last.isIntermediate)) { !translationHistory.last.isIntermediate)) {
currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString(); currentSessionId ??= DateTime.now().millisecondsSinceEpoch.toString();
// 关键调整:使用与最终结果一致的语言方向,避免左右“跳变”
final languageCodes = _determineLanguageCodes();
final newItem = TranslationItem( final newItem = TranslationItem(
sourceText: text, sourceText: text,
translatedText: '', translatedText: '',
sourceLanguageCode: sourceLanguageCode.value, sourceLanguageCode: languageCodes['source']!,
targetLanguageCode: targetLanguageCode.value, targetLanguageCode: languageCodes['target']!,
timestamp: DateTime.now(), timestamp: DateTime.now(),
sessionId: currentSessionId!, sessionId: currentSessionId!,
isFirstInSession: translationHistory.isEmpty || isFirstInSession: translationHistory.isEmpty ||

34
lib/modules/translation/controllers/translation_history_controller.dart

@ -15,6 +15,12 @@ class TranslationHistoryManager extends GetxController {
static const int _maxStorageItems = 1000; // 存储限制 static const int _maxStorageItems = 1000; // 存储限制
static const int _maxDisplayItems = 20; // 显示限制 static const int _maxDisplayItems = 20; // 显示限制
String _currentMode = 'simultaneous'; // 当前模式
String _getHistoryKey() => '${_historyKey}_$_currentMode'; // 模式化存储键
void setMode(String mode) {
_currentMode = mode;
}
final GetStorage _storage = GetStorage(); final GetStorage _storage = GetStorage();
// 翻译历史数据 // 翻译历史数据
@ -40,7 +46,7 @@ class TranslationHistoryManager extends GetxController {
/// 加载翻译历史记录 /// 加载翻译历史记录
/// 初始化时只加载最近的显示条数,提高性能 /// 初始化时只加载最近的显示条数,提高性能
void loadTranslationHistory() { void loadTranslationHistory() {
final historyJson = _storage.read(_historyKey); final historyJson = _storage.read(_getHistoryKey());
if (historyJson != null) { if (historyJson != null) {
try { try {
final List<dynamic> historyList = jsonDecode(historyJson); final List<dynamic> historyList = jsonDecode(historyJson);
@ -64,15 +70,15 @@ class TranslationHistoryManager extends GetxController {
Logger.info('翻译历史加载成功: ${translationHistory.length}条记录'); Logger.info('翻译历史加载成功: ${translationHistory.length}条记录');
} catch (e) { } catch (e) {
_storage.remove(_historyKey); _storage.remove(_getHistoryKey());
Logger.error('加载翻译历史失败: ${e.toString()}'); Logger.error('加载翻译历史失败: ${e.toString()}');
} }
} else { } else {
// 没有历史数据时初始化分页状态
hasMoreHistory.value = false; hasMoreHistory.value = false;
currentPage.value = 1; currentPage.value = 1;
isLoadingMore.value = false; isLoadingMore.value = false;
isLoadingMoreError.value = false; isLoadingMoreError.value = false;
translationHistory.clear();
} }
} }
@ -94,7 +100,7 @@ class TranslationHistoryManager extends GetxController {
// 保存到本地存储 // 保存到本地存储
final List<Map<String, dynamic>> historyJson = final List<Map<String, dynamic>> historyJson =
historyToSave.map((item) => item.toJson()).toList(); historyToSave.map((item) => item.toJson()).toList();
_storage.write(_historyKey, jsonEncode(historyJson)); _storage.write(_getHistoryKey(), jsonEncode(historyJson));
Logger.info('翻译历史保存成功: 存储${historyToSave.length}条记录'); Logger.info('翻译历史保存成功: 存储${historyToSave.length}条记录');
} catch (e) { } catch (e) {
@ -141,7 +147,7 @@ class TranslationHistoryManager extends GetxController {
Logger.i(TAG, '正在加载第 $nextPage 页翻译历史...'); Logger.i(TAG, '正在加载第 $nextPage 页翻译历史...');
// 从存储中获取所有历史记录 // 从存储中获取所有历史记录
final historyJson = _storage.read(_historyKey); final historyJson = _storage.read(_getHistoryKey());
if (historyJson != null) { if (historyJson != null) {
final List<dynamic> historyList = jsonDecode(historyJson); final List<dynamic> historyList = jsonDecode(historyJson);
final List<TranslationItem> allHistory = final List<TranslationItem> allHistory =
@ -194,8 +200,8 @@ class TranslationHistoryManager extends GetxController {
final List<TranslationItem> result = <TranslationItem>[]; final List<TranslationItem> result = <TranslationItem>[];
final Set<String> existingIds = <String>{}; final Set<String> existingIds = <String>{};
// 添加存储的历史记录 // 添加存储的历史记录(按当前模式)
final historyJson = _storage.read(_historyKey); final historyJson = _storage.read(_getHistoryKey());
if (historyJson != null) { if (historyJson != null) {
final List<dynamic> historyList = jsonDecode(historyJson); final List<dynamic> historyList = jsonDecode(historyJson);
final List<TranslationItem> storedHistory = final List<TranslationItem> storedHistory =
@ -235,7 +241,7 @@ class TranslationHistoryManager extends GetxController {
/// 清除所有翻译历史 /// 清除所有翻译历史
void clearTranslationHistory() { void clearTranslationHistory() {
translationHistory.clear(); translationHistory.clear();
_storage.remove(_historyKey); _storage.remove(_getHistoryKey());
Logger.info('翻译历史已清除'); Logger.info('翻译历史已清除');
} }
@ -247,8 +253,8 @@ class TranslationHistoryManager extends GetxController {
translationHistory.removeWhere((item) => item.sessionId == sessionId); translationHistory.removeWhere((item) => item.sessionId == sessionId);
final removedFromMemory = originalCount - translationHistory.length; final removedFromMemory = originalCount - translationHistory.length;
// 从存储中删除 // 从存储中删除(按当前模式)
final historyJson = _storage.read(_historyKey); final historyJson = _storage.read(_getHistoryKey());
int removedFromStorage = 0; int removedFromStorage = 0;
if (historyJson != null) { if (historyJson != null) {
@ -263,7 +269,7 @@ class TranslationHistoryManager extends GetxController {
// 重新保存 // 重新保存
final List<Map<String, dynamic>> updatedHistoryJson = final List<Map<String, dynamic>> updatedHistoryJson =
storedHistory.map((item) => item.toJson()).toList(); storedHistory.map((item) => item.toJson()).toList();
_storage.write(_historyKey, jsonEncode(updatedHistoryJson)); _storage.write(_getHistoryKey(), jsonEncode(updatedHistoryJson));
} }
translationHistory.refresh(); translationHistory.refresh();
@ -287,8 +293,8 @@ class TranslationHistoryManager extends GetxController {
timestamp.millisecondsSinceEpoch); timestamp.millisecondsSinceEpoch);
final removedFromMemory = originalCount - translationHistory.length; final removedFromMemory = originalCount - translationHistory.length;
// 从存储中删除 // 从存储中删除(按当前模式)
final historyJson = _storage.read(_historyKey); final historyJson = _storage.read(_getHistoryKey());
int removedFromStorage = 0; int removedFromStorage = 0;
if (historyJson != null) { if (historyJson != null) {
@ -306,7 +312,7 @@ class TranslationHistoryManager extends GetxController {
// 重新保存 // 重新保存
final List<Map<String, dynamic>> updatedHistoryJson = final List<Map<String, dynamic>> updatedHistoryJson =
storedHistory.map((item) => item.toJson()).toList(); storedHistory.map((item) => item.toJson()).toList();
_storage.write(_historyKey, jsonEncode(updatedHistoryJson)); _storage.write(_getHistoryKey(), jsonEncode(updatedHistoryJson));
} }
translationHistory.refresh(); translationHistory.refresh();

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

@ -80,6 +80,31 @@ class TranslationView extends GetView<TranslationController> {
controller: controller, controller: controller,
), ),
Positioned(
top: 2,
left: 0,
right: 0,
child: SafeArea(
top: true,
child: Container(
height: 56.h,
padding: EdgeInsets.symmetric(horizontal: 8.w),
color: Colors.transparent,
child: Align(
alignment: Alignment.centerLeft,
child: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
onPressed: () => Get.back(),
),
),
),
),
),
// 顶部按钮(倒过来) // 顶部按钮(倒过来)
Positioned( Positioned(
top: 50.h, top: 50.h,

130
lib/modules/translation/views/widgets/face_to_face_layout.dart

@ -7,7 +7,7 @@ import '../../../../core/theme/app_colors.dart';
import '../dialogs/language_selection_dialog.dart'; import '../dialogs/language_selection_dialog.dart';
/// 面对面翻译布局组件 /// 面对面翻译布局组件
class FaceToFaceLayout extends StatelessWidget { class FaceToFaceLayout extends StatefulWidget {
final bool isDarkMode; final bool isDarkMode;
final TranslationController controller; final TranslationController controller;
@ -17,13 +17,60 @@ class FaceToFaceLayout extends StatelessWidget {
required this.controller, required this.controller,
}) : super(key: key); }) : super(key: key);
@override
State<FaceToFaceLayout> createState() => _FaceToFaceLayoutState();
}
class _FaceToFaceLayoutState extends State<FaceToFaceLayout> {
final ScrollController _topScrollController = ScrollController();
final ScrollController _bottomScrollController = ScrollController();
@override
void dispose() {
_topScrollController.dispose();
_bottomScrollController.dispose();
super.dispose();
}
void _scrollToBottom({bool animate = false}) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_topScrollController.hasClients) {
final m = _topScrollController.position.maxScrollExtent;
if (animate) {
_topScrollController.animateTo(
m,
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
);
} else {
_topScrollController.jumpTo(m);
}
}
if (_bottomScrollController.hasClients) {
final m2 = _bottomScrollController.position.maxScrollExtent;
if (animate) {
_bottomScrollController.animateTo(
m2,
duration: const Duration(milliseconds: 200),
curve: Curves.easeOut,
);
} else {
_bottomScrollController.jumpTo(m2);
}
}
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDarkMode = widget.isDarkMode;
final controller = widget.controller;
return Obx(() { return Obx(() {
// 获取最新的翻译记录 // 获取最新的翻译记录
final latestItem = controller.translationHistory.isNotEmpty final latestItem = controller.translationHistory.isNotEmpty
? controller.translationHistory.last ? controller.translationHistory.last
: null; : null;
_scrollToBottom(animate: false);
return Column( return Column(
children: [ children: [
@ -33,7 +80,7 @@ class FaceToFaceLayout extends StatelessWidget {
angle: 3.14159, // 180度 = π弧度 angle: 3.14159, // 180度 = π弧度
child: Container( child: Container(
width: double.infinity, width: double.infinity,
padding: EdgeInsets.all(24.w), padding: EdgeInsets.fromLTRB(24.w, 24.w, 24.w, 24.w + 96.h),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode color: isDarkMode
? AppColors.cardBackground.withValues(alpha: 0.5) ? AppColors.cardBackground.withValues(alpha: 0.5)
@ -45,24 +92,22 @@ class FaceToFaceLayout extends StatelessWidget {
), ),
), ),
), ),
child: Column( child: SingleChildScrollView(
mainAxisAlignment: MainAxisAlignment.center, controller: _topScrollController,
children: [ physics: const BouncingScrollPhysics(),
// 上半部的文本 child: Text(
Text( latestItem?.targetLanguageCode ==
latestItem?.targetLanguageCode == controller.targetLanguageCode.value
controller.targetLanguageCode.value ? latestItem?.translatedText ?? ''
? latestItem?.translatedText ?? '' //如果为null则显示空字符 : latestItem?.sourceText ?? '',
: latestItem?.sourceText ?? '', style: TextStyle(
style: TextStyle( fontSize: 20.sp,
fontSize: 20.sp, fontWeight: FontWeight.w500,
fontWeight: FontWeight.w500, color: isDarkMode ? Colors.white : Colors.black87,
color: isDarkMode ? Colors.white : Colors.black87, height: 1.4,
height: 1.4,
),
textAlign: TextAlign.center,
), ),
], textAlign: TextAlign.center,
),
), ),
), ),
), ),
@ -75,33 +120,31 @@ class FaceToFaceLayout extends StatelessWidget {
Expanded( Expanded(
child: Container( child: Container(
width: double.infinity, width: double.infinity,
padding: EdgeInsets.all(24.w), padding: EdgeInsets.fromLTRB(24.w, 24.w, 24.w, 24.w + 96.h),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode color: isDarkMode
? AppColors.cardBackground.withValues(alpha: 0.5) ? AppColors.cardBackground.withValues(alpha: 0.5)
: Colors.green[50]?.withValues(alpha: 0.3), : Colors.green[50]?.withValues(alpha: 0.3),
), ),
child: Column( child: SingleChildScrollView(
mainAxisAlignment: MainAxisAlignment.center, controller: _bottomScrollController,
children: [ physics: const BouncingScrollPhysics(),
// 下半部的文本 child: Text(
Text( latestItem?.sourceLanguageCode ==
latestItem?.sourceLanguageCode == controller.sourceLanguageCode.value
controller.sourceLanguageCode.value ? latestItem?.sourceText ??
? latestItem?.sourceText ?? (controller.faceToFaceRecognitionText.value.isEmpty
(controller.faceToFaceRecognitionText.value.isEmpty ? ''
? '' : controller.faceToFaceRecognitionText.value)
: controller.faceToFaceRecognitionText.value) : latestItem?.translatedText ?? '',
: latestItem?.translatedText ?? '', style: TextStyle(
style: TextStyle( fontSize: 20.sp,
fontSize: 20.sp, fontWeight: FontWeight.w500,
fontWeight: FontWeight.w500, color: isDarkMode ? Colors.white : Colors.black87,
color: isDarkMode ? Colors.white : Colors.black87, height: 1.4,
height: 1.4,
),
textAlign: TextAlign.center,
), ),
], textAlign: TextAlign.center,
),
), ),
), ),
), ),
@ -146,7 +189,7 @@ class FaceToFaceLayout extends StatelessWidget {
borderRadius: BorderRadius.circular(16.r), borderRadius: BorderRadius.circular(16.r),
), ),
child: Text( child: Text(
'${controller.sourceLanguage.value.split(' ')[0]}'.tr, '${widget.controller.sourceLanguage.value.split(' ')[0]}'.tr,
style: TextStyle( style: TextStyle(
fontSize: 15.sp, fontSize: 15.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -161,7 +204,7 @@ class FaceToFaceLayout extends StatelessWidget {
// 语言互换按钮 // 语言互换按钮
GestureDetector( GestureDetector(
onTap: controller.swapLanguages, onTap: widget.controller.swapLanguages,
child: Container( child: Container(
margin: EdgeInsets.symmetric(horizontal: 8.w), margin: EdgeInsets.symmetric(horizontal: 8.w),
padding: EdgeInsets.all(4.r), padding: EdgeInsets.all(4.r),
@ -194,7 +237,8 @@ class FaceToFaceLayout extends StatelessWidget {
child: Transform.rotate( child: Transform.rotate(
angle: 3.14159, // 旋转180度 angle: 3.14159, // 旋转180度
child: Text( child: Text(
'${controller.targetLanguage.value.split(' ')[0]}'.tr, '${widget.controller.targetLanguage.value.split(' ')[0]}'
.tr,
style: TextStyle( style: TextStyle(
fontSize: 15.sp, fontSize: 15.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,

10
lib/modules/translation/views/widgets/translation_item_widget.dart

@ -49,15 +49,19 @@ class TranslationItemWidget extends GetView<TranslationController> {
// 判断是否是己方的翻译项(依据控制器当前源语言) // 判断是否是己方的翻译项(依据控制器当前源语言)
final bool isSelf = final bool isSelf =
item.sourceLanguageCode == controller.sourceLanguageCode.value; item.sourceLanguageCode == controller.sourceLanguageCode.value;
// 新增:通话模式标记,用于控制右侧显示与隐藏中间态
final bool isCallMode = controller.currentMode.value == 'call';
return GestureDetector( return GestureDetector(
onTap: () => onToggleActionItem(item), // 触碰气泡时切换按钮显隐 onTap: () => onToggleActionItem(item), // 触碰气泡时切换按钮显隐
onLongPressStart: (details) => onLongPressStart: (details) =>
onLongPressStart(context, isDarkMode, details, item), onLongPressStart(context, isDarkMode, details, item),
child: Align( child: Align(
alignment: item.isIntermediate alignment: isCallMode
? Alignment.center ? (isSelf ? Alignment.centerLeft : Alignment.centerRight)
: (isSelf ? Alignment.centerLeft : Alignment.centerRight), : item.isIntermediate
? Alignment.center
: (isSelf ? Alignment.centerLeft : Alignment.centerRight),
child: Container( child: Container(
margin: EdgeInsets.only(bottom: 8.h), margin: EdgeInsets.only(bottom: 8.h),
padding: EdgeInsets.all(12.w), padding: EdgeInsets.all(12.w),

10
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt

@ -219,11 +219,11 @@ class BleCommandSender {
// 正常响应处理 // 正常响应处理
val length = data[2].toInt() and 0xFF val length = data[2].toInt() and 0xFF
// 检查数据长度 // // 检查数据长度
if (data.size != length + 4) { // 帧头 + 命令 + 长度 + 数据 + CRC // if (data.size != length + 4) { // 帧头 + 命令 + 长度 + 数据 + CRC
Log.e(TAG, "设备响应数据长度不匹配: 期望=${length + 4}, 实际=${data.size}") // Log.e(TAG, "设备响应数据长度不匹配: 期望=${length + 4}, 实际=${data.size}")
return // return
} // }
// 计算CRC校验 - 修正:计算除最后一个CRC字节外的所有字节 // 计算CRC校验 - 修正:计算除最后一个CRC字节外的所有字节
val payload = data.sliceArray(0 until data.size - 1) val payload = data.sliceArray(0 until data.size - 1)

2
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt

@ -893,12 +893,14 @@ object BleService {
// 统计接收到的字节数 // 统计接收到的字节数
bytesReceivedInCurrentSecond += data.size bytesReceivedInCurrentSecond += data.size
processAudioData(data) processAudioData(data)
Log.i(TAG, "lxm--收到音频特征数据, 字节数=${data.size}")
} }
BleConst.CALL_RECEIVE_AUDIO_CHAR_UUID -> { BleConst.CALL_RECEIVE_AUDIO_CHAR_UUID -> {
// 统计接收到的字节数 // 统计接收到的字节数
bytesReceivedInCurrentSecond += data.size bytesReceivedInCurrentSecond += data.size
processAudioData(data) processAudioData(data)
Log.i(TAG, "lxm--收到通话音频特征数据, 字节数=${data.size}")
} }
// 通知特征数据(命令和控制) // 通知特征数据(命令和控制)
BleConst.NOTIFY_CHAR_UUID -> { BleConst.NOTIFY_CHAR_UUID -> {

51
local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift

@ -282,22 +282,11 @@ private var cmdReplyType: UInt8 = 0
// 处理传入的UUID参数 // 处理传入的UUID参数
for uuidStr in uuids { for uuidStr in uuids {
do { if let cbuuid = parseCBUUID(from: uuidStr) {
let cbuuid: CBUUID
if uuidStr.count == 4 {
// 短UUID,直接使用
cbuuid = CBUUID(string: uuidStr)
} else if uuidStr.count == 36 {
// 完整UUID
cbuuid = CBUUID(string: uuidStr)
} else {
os_log("⚠️ 无效的UUID格式: %{public}@", log: logger, type: .default, uuidStr)
continue
}
serviceUUIDs.append(cbuuid) serviceUUIDs.append(cbuuid)
os_log("✅ 添加扫描过滤器UUID: %{public}@", log: logger, type: .info, uuidStr) os_log("✅ 添加扫描过滤器UUID: %{public}@", log: logger, type: .info, uuidStr)
} catch { } else {
os_log("❌ 构造UUID失败: %{public}@, 错误: %{public}@", log: logger, type: .error, uuidStr, error.localizedDescription) os_log("⚠️ 无效的UUID格式或字符: %{public}@", log: logger, type: .error, uuidStr)
} }
} }
@ -319,6 +308,40 @@ private var cmdReplyType: UInt8 = 0
} }
return true return true
} }
/// 验证并解析 UUID 字符串为 CBUUID
/// - Parameter uuidStr: UUID 字符串(支持 4 位短 UUID、36 位完整 UUID;扩展支持 32 位不带短横线 UUID)
/// - Returns: 解析成功返回 CBUUID,否则返回 nil
private func parseCBUUID(from uuidStr: String) -> CBUUID? {
let trimmed = uuidStr.trimmingCharacters(in: .whitespacesAndNewlines)
// 4 位短 UUID:仅允许十六进制字符,通过正则校验
if trimmed.count == 4 {
if trimmed.range(of: "^[0-9a-fA-F]{4}$", options: .regularExpression) != nil {
return CBUUID(string: trimmed.lowercased())
} else {
return nil
}
}
// 36 位完整 UUID:使用系统 UUID 进行格式校验
if trimmed.count == 36, let nsuuid = UUID(uuidString: trimmed) {
return CBUUID(nsuuid: nsuuid)
}
// 32 位完整 UUID(不带短横线):按 8-4-4-4-12 格式插入短横线后校验
if trimmed.count == 32,
trimmed.range(of: "^[0-9a-fA-F]{32}$", options: .regularExpression) != nil {
let lower = trimmed.lowercased()
let formatted = "\(lower.prefix(8))-\(lower.dropFirst(8).prefix(4))-\(lower.dropFirst(12).prefix(4))-\(lower.dropFirst(16).prefix(4))-\(lower.dropFirst(20))"
if let nsuuid = UUID(uuidString: formatted) {
return CBUUID(nsuuid: nsuuid)
}
}
// 其他长度不支持
return nil
}
/// 停止扫描 /// 停止扫描
public func stopScan() { public func stopScan() {

Loading…
Cancel
Save