Browse Source

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

weicu
liwei1dao 11 months ago
parent
commit
35dbed3352
  1. 51
      android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowService.kt
  2. BIN
      assets/images/DefaultPicture.jpg
  3. BIN
      assets/images/defaultPicture.png
  4. BIN
      assets/images/headphone_new3.png
  5. BIN
      assets/images/headphone_new4.png
  6. 5
      ios/Runner/RunnerProfile.entitlements
  7. 8
      lib/core/bindings/initial_binding.dart
  8. 4
      lib/core/widgets/common_bottom_nav.dart
  9. 2
      lib/data/models/appconfig_model.dart
  10. 2
      lib/data/models/appconfig_model.g.dart
  11. 5
      lib/data/services/account_service.dart
  12. 88
      lib/data/services/ble_manager.dart
  13. 4
      lib/data/services/deapsound_ai_service.dart
  14. 215
      lib/data/services/meeting/meeting_task_service.dart
  15. 7
      lib/data/services/meeting/meeting_upload_service.dart
  16. 160
      lib/data/services/meeting/summary_task_service.dart
  17. 18
      lib/data/services/ota_check_service.dart
  18. 10
      lib/data/services/pay_service.dart
  19. 545
      lib/data/services/translation_usage_service.dart
  20. 12
      lib/modules/agent/views/message_bubble.dart
  21. 2
      lib/modules/agent/views/qqmusic_card.dart
  22. 2
      lib/modules/devices/controllers/devices_controller.dart
  23. 171
      lib/modules/devices/views/devices_view.dart
  24. 10
      lib/modules/eq_settings/bindings/eq_settings_binding.dart
  25. 217
      lib/modules/eq_settings/controllers/eq_settings_controller.dart
  26. 95
      lib/modules/eq_settings/views/eq_settings_view.dart
  27. 96
      lib/modules/eq_settings/views/widgets/row_confirm_widget.dart
  28. 83
      lib/modules/eq_settings/views/widgets/row_equalizer_widget.dart
  29. 63
      lib/modules/eq_settings/views/widgets/row_grade_widget.dart
  30. 76
      lib/modules/eq_settings/views/widgets/row_input_widget.dart
  31. 242
      lib/modules/eq_settings/views/widgets/row_keypress_widget.dart
  32. 187
      lib/modules/eq_settings/views/widgets/row_radio_widget.dart
  33. 61
      lib/modules/eq_settings/views/widgets/row_switch_widget.dart
  34. 77
      lib/modules/eq_settings/views/widgets/row_timer_widget.dart
  35. 305
      lib/modules/eq_settings/views/widgets/select_equalizer_widget.dart
  36. 231
      lib/modules/eq_settings/views/widgets/select_timer_widget.dart
  37. 162
      lib/modules/eq_settings/views/widgets/settings_input_widget.dart
  38. 6
      lib/modules/home/controllers/home_controller.dart
  39. 56
      lib/modules/home/views/home_view.dart
  40. 9
      lib/modules/meeting/controllers/meeting_details_controller.dart
  41. 47
      lib/modules/meeting/controllers/meeting_home_controller.dart
  42. 38
      lib/modules/meeting/controllers/mind_map_controller.dart
  43. 28
      lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart
  44. 13
      lib/modules/ota_upgrade/controllers/ota_core_contropller.dart
  45. 87
      lib/modules/ota_upgrade/views/ota_upgrade_view.dart
  46. 4
      lib/modules/pairing/controllers/pairing_controller.dart
  47. 6
      lib/modules/qqmusic/views/qqmusic_playlist_view.dart
  48. 11
      lib/modules/splash/controllers/splash_controller.dart
  49. 323
      lib/modules/translation/controllers/translation_controller.dart
  50. 34
      lib/modules/translation/controllers/translation_history_controller.dart
  51. 2
      lib/modules/translation/views/dialogs/translation_long_press_menu.dart
  52. 466
      lib/modules/translation/views/translation_view.dart
  53. 130
      lib/modules/translation/views/widgets/face_to_face_layout.dart
  54. 195
      lib/modules/translation/views/widgets/translation_item_widget.dart
  55. 1
      lib/routes/app_routes.dart
  56. 96
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCommandSender.kt
  57. 3
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt
  58. 20
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt
  59. 68
      local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OpusAudioManager.kt
  60. 2
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift
  61. 221
      local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleService.swift
  62. 33
      local_plugins/native_plugin/.gitignore
  63. 33
      local_plugins/native_plugin/.metadata
  64. 3
      local_plugins/native_plugin/CHANGELOG.md
  65. 1
      local_plugins/native_plugin/LICENSE
  66. 15
      local_plugins/native_plugin/README.md
  67. 4
      local_plugins/native_plugin/analysis_options.yaml
  68. 9
      local_plugins/native_plugin/android/.gitignore
  69. 52
      local_plugins/native_plugin/android/build.gradle
  70. 1
      local_plugins/native_plugin/android/settings.gradle
  71. 3
      local_plugins/native_plugin/android/src/main/AndroidManifest.xml
  72. 38
      local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/NativePlugin.java
  73. 29
      local_plugins/native_plugin/android/src/test/java/com/example/native_plugin/NativePluginTest.java
  74. 38
      local_plugins/native_plugin/ios/.gitignore
  75. 0
      local_plugins/native_plugin/ios/Assets/.gitkeep
  76. 15
      local_plugins/native_plugin/ios/Classes/FLNativeView.h
  77. 83
      local_plugins/native_plugin/ios/Classes/FLNativeView.m
  78. 4
      local_plugins/native_plugin/ios/Classes/NativePlugin.h
  79. 114
      local_plugins/native_plugin/ios/Classes/NativePlugin.m
  80. 15
      local_plugins/native_plugin/ios/Classes/PlayerView.h
  81. 100
      local_plugins/native_plugin/ios/Classes/PlayerView.m
  82. 14
      local_plugins/native_plugin/ios/Resources/PrivacyInfo.xcprivacy
  83. 29
      local_plugins/native_plugin/ios/native_plugin.podspec
  84. 19
      local_plugins/native_plugin/lib/native_plugin.dart
  85. 37
      local_plugins/native_plugin/lib/native_plugin_method_channel.dart
  86. 47
      local_plugins/native_plugin/lib/native_plugin_platform_interface.dart
  87. 72
      local_plugins/native_plugin/pubspec.yaml
  88. 10
      pubspec.yaml

51
android/app/src/main/kotlin/com/saitong/voitrans/FloatingWindowService.kt

@ -19,6 +19,7 @@ class FloatingWindowService : Service() {
private var windowManager: WindowManager? = null private var windowManager: WindowManager? = null
private var floatingView: View? = null private var floatingView: View? = null
private var isMinimized = true private var isMinimized = true
private lateinit var layoutParams: WindowManager.LayoutParams
private lateinit var sourceTextView: TextView private lateinit var sourceTextView: TextView
private lateinit var translatedTextView: TextView private lateinit var translatedTextView: TextView
@ -73,7 +74,7 @@ class FloatingWindowService : Service() {
setupClickListeners() setupClickListeners()
// 设置窗口参数 // 设置窗口参数
val layoutParams = WindowManager.LayoutParams().apply { layoutParams = WindowManager.LayoutParams().apply {
width = WindowManager.LayoutParams.WRAP_CONTENT width = WindowManager.LayoutParams.WRAP_CONTENT
height = WindowManager.LayoutParams.WRAP_CONTENT height = WindowManager.LayoutParams.WRAP_CONTENT
type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -255,26 +256,12 @@ class FloatingWindowService : Service() {
} }
private fun setupClickListeners() { private fun setupClickListeners() {
// 最小化状态点击展开
minimizedView.setOnClickListener {
isMinimized = false
updateViewState(false)
notifyStateChange("expanded")
}
// 展开状态点击最小化
expandedView.setOnClickListener {
isMinimized = true
updateViewState(true)
notifyStateChange("minimized")
}
// 长按关闭 // 长按关闭
floatingView!!.setOnLongClickListener { floatingView!!.setOnLongClickListener {
closeFloatingWindow() closeFloatingWindow()
true true
} }
// 拖拽功能 // 拖拽功能
setupDragListener() setupDragListener()
} }
@ -284,25 +271,40 @@ class FloatingWindowService : Service() {
var initialY = 0 var initialY = 0
var initialTouchX = 0f var initialTouchX = 0f
var initialTouchY = 0f var initialTouchY = 0f
var dragging = false
val threshold = (4 * resources.displayMetrics.density).toInt()
floatingView!!.setOnTouchListener { _, event -> floatingView!!.setOnTouchListener { _, event ->
when (event.action) { when (event.action) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
val layoutParams = floatingView!!.layoutParams as WindowManager.LayoutParams
initialX = layoutParams.x initialX = layoutParams.x
initialY = layoutParams.y initialY = layoutParams.y
initialTouchX = event.rawX initialTouchX = event.rawX
initialTouchY = event.rawY initialTouchY = event.rawY
dragging = false
true true
} }
MotionEvent.ACTION_MOVE -> { MotionEvent.ACTION_MOVE -> {
val layoutParams = floatingView!!.layoutParams as WindowManager.LayoutParams val dx = (event.rawX - initialTouchX).toInt()
layoutParams.x = initialX + (event.rawX - initialTouchX).toInt() val dy = (event.rawY - initialTouchY).toInt()
layoutParams.y = initialY + (event.rawY - initialTouchY).toInt() if (Math.abs(dx) > threshold || Math.abs(dy) > threshold) dragging = true
val dm = resources.displayMetrics
val maxX = dm.widthPixels - (floatingView?.width ?: 0)
val maxY = dm.heightPixels - (floatingView?.height ?: 0)
layoutParams.x = (initialX + dx).coerceIn(0, maxX)
layoutParams.y = (initialY + dy).coerceIn(0, maxY)
windowManager?.updateViewLayout(floatingView, layoutParams) windowManager?.updateViewLayout(floatingView, layoutParams)
true true
} }
else -> false MotionEvent.ACTION_UP -> {
if (!dragging) {
isMinimized = !isMinimized
updateViewState(isMinimized)
notifyStateChange(if (isMinimized) "minimized" else "expanded")
}
dragging = false
true
}
else -> true
} }
} }
} }
@ -347,11 +349,10 @@ class FloatingWindowService : Service() {
val x = intent.getIntExtra(EXTRA_X, 0) val x = intent.getIntExtra(EXTRA_X, 0)
val y = intent.getIntExtra(EXTRA_Y, 0) val y = intent.getIntExtra(EXTRA_Y, 0)
floatingView?.let { view -> floatingView?.let {
val layoutParams = view.layoutParams as WindowManager.LayoutParams
layoutParams.x = x layoutParams.x = x
layoutParams.y = y layoutParams.y = y
windowManager?.updateViewLayout(view, layoutParams) windowManager?.updateViewLayout(it, layoutParams)
} }
} }

BIN
assets/images/DefaultPicture.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

BIN
assets/images/defaultPicture.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 KiB

BIN
assets/images/headphone_new3.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 774 KiB

BIN
assets/images/headphone_new4.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 904 KiB

5
ios/Runner/RunnerProfile.entitlements

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

8
lib/core/bindings/initial_binding.dart

@ -1,5 +1,6 @@
import 'package:voitrans/data/services/pay_service.dart'; import '../../data/services/meeting/summary_task_service.dart';
import 'package:voitrans/data/services/qq_music.dart'; import '/data/services/pay_service.dart';
import '/data/services/qq_music.dart';
import '../../../data/services/user_portrait.dart'; import '../../../data/services/user_portrait.dart';
@ -70,6 +71,9 @@ class InitialBinding extends Bindings {
// 会议助手任务服务 // 会议助手任务服务
Get.lazyPut<MeetingTaskService>(() => MeetingTaskService(), fenix: true); Get.lazyPut<MeetingTaskService>(() => MeetingTaskService(), fenix: true);
// 会议助手总结任务服务
Get.lazyPut<SummaryTaskService>(() => SummaryTaskService(), fenix: true);
// 会议助手音频上传 // 会议助手音频上传
Get.lazyPut<MeetingUploadService>(() => MeetingUploadService(), Get.lazyPut<MeetingUploadService>(() => MeetingUploadService(),
fenix: true); fenix: true);

4
lib/core/widgets/common_bottom_nav.dart

@ -17,8 +17,8 @@ class CommonBottomNav extends StatelessWidget {
return BottomNavigationBar( return BottomNavigationBar(
currentIndex: currentIndex, currentIndex: currentIndex,
onTap: (index) { onTap: (index) {
// Navigate to the appropriate page based on index // 点击当前项不执行跳转
if(index ==currentIndex) return;//如果是当前页面,不跳转 if (index == currentIndex) return; //如果是当前页面,不跳转
switch (index) { switch (index) {
case 0: case 0:
Get.offAllNamed(Routes.home); Get.offAllNamed(Routes.home);

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,

5
lib/data/services/account_service.dart

@ -6,6 +6,7 @@ import '../../modules/login/controllers/googleLogin.dart';
import '../../modules/login/controllers/facebookLogin.dart'; import '../../modules/login/controllers/facebookLogin.dart';
import '../services/network/api.dart'; import '../services/network/api.dart';
import 'db/database_helper.dart'; import 'db/database_helper.dart';
import 'meeting/meeting_task_service.dart';
/// 账号管理服务 /// 账号管理服务
/// ///
@ -79,6 +80,10 @@ class AccountService extends GetxService {
/// 返回操作结果,包含成功状态和错误信息 /// 返回操作结果,包含成功状态和错误信息
Future<AccountOperationResult> signOut() async { Future<AccountOperationResult> signOut() async {
try { try {
// 清除会议记录任务列表
final taskService = Get.find<MeetingTaskService>();
taskService.cancelTask();
// 上传会议记录操作记录 // 上传会议记录操作记录
await Synchrodata.uploadOperationRecords(); await Synchrodata.uploadOperationRecords();

88
lib/data/services/ble_manager.dart

@ -8,8 +8,8 @@ import 'package:get_storage/get_storage.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:ble_service/ble_service.dart'; import 'package:ble_service/ble_service.dart';
import 'package:agent_service/agent_service.dart'; import 'package:agent_service/agent_service.dart';
import '../../data/models/appconfig_model.dart'; import '/data/models/appconfig_model.dart';
import '../../data/models/user_Info.dart' as userInfo;
import '../../data/models/appconfig.dart'; import '../../data/models/appconfig.dart';
import '../../core/utils/logger.dart'; import '../../core/utils/logger.dart';
import 'bluetooth_manager.dart'; import 'bluetooth_manager.dart';
@ -27,6 +27,67 @@ class BleCommands {
static const int CONTROL_CODEC = 0x05; static const int CONTROL_CODEC = 0x05;
} }
/// 判断编解码控制指令是否成功
///
/// 根据 rawData 第二字节是否为非 0 来判断:
/// - 成功:第二字节不为 0
/// - 失败或无法解析:返回 false
bool _isCodecControlSuccess(Map<dynamic, dynamic> data) {
final dynamic raw = data['rawData'];
if (raw == null) return false;
int? secondByte;
if (raw is List) {
if (raw.length >= 2) {
final s = raw[1];
if (s is int) {
secondByte = s;
} else if (s is num) {
secondByte = s.toInt();
} else if (s is String) {
final t = s.trim();
if (t.startsWith('0x') || t.startsWith('0X')) {
secondByte = int.tryParse(t.substring(2), radix: 16);
} else {
secondByte = int.tryParse(t);
}
}
}
} else if (raw is String) {
final normalized = raw.trim().replaceAll('[', '').replaceAll(']', '');
final parts =
normalized.split(RegExp(r'[,\s]+')).where((e) => e.isNotEmpty).toList();
if (parts.length >= 2) {
final t = parts[1].trim();
if (t.startsWith('0x') || t.startsWith('0X')) {
secondByte = int.tryParse(t.substring(2), radix: 16);
} else {
secondByte = int.tryParse(t);
}
}
} else if (raw is Map) {
final dynamic s = raw['second'] ?? raw['status2'] ?? raw['result'];
if (s is int) {
secondByte = s;
} else if (s is num) {
secondByte = s.toInt();
} else if (s is String) {
final t = s.trim();
if (t.startsWith('0x') || t.startsWith('0X')) {
secondByte = int.tryParse(t.substring(2), radix: 16);
} else {
secondByte = int.tryParse(t);
}
}
} else {
// 无法解析的类型
return false;
}
return secondByte != null && secondByte != 0;
}
/// 蓝牙设备信息缓存模型 /// 蓝牙设备信息缓存模型
class DeviceInfo { class DeviceInfo {
final String address; final String address;
@ -135,6 +196,11 @@ class BleManager extends GetxService {
// 设备信息缓存 // 设备信息缓存
final deviceInfo = Rx<DeviceInfo?>(null); // 使用 Rx 包装 final deviceInfo = Rx<DeviceInfo?>(null); // 使用 Rx 包装
//添加编码器状态跟踪
final _isCodecActive = false.obs;
bool get isCodecActive => _isCodecActive.value;
RxBool get isCodecActiveRx => _isCodecActive;
// 订阅 // 订阅
StreamSubscription<dynamic>? _statusSubscription; StreamSubscription<dynamic>? _statusSubscription;
StreamSubscription<dynamic>? _dataSubscription; StreamSubscription<dynamic>? _dataSubscription;
@ -306,7 +372,18 @@ class BleManager extends GetxService {
Logger.info('lxm--处理编解码控制命令'); Logger.info('lxm--处理编解码控制命令');
// 处理通话状态 // 处理通话状态
print(" CONTROL_CODEC: $commandId,$data"); print(" CONTROL_CODEC: $commandId,$data");
//_isCall.value = data['callStatus'] as bool? ?? false;
final bool codecSuccess = _isCodecControlSuccess(data);
_isCodecActive.value = codecSuccess; // 更新编解码器状态
if (codecSuccess) {
print("lxm--编解码控制成功 (rawData 第二字节非 0)");
Logger.info('lxm--编解码控制成功 (rawData 第二字节非 0)');
} else {
print("lxm--编解码控制失败 (rawData 为 [162, 0] 或无法解析)");
Logger.warning('lxm--编解码控制失败 (rawData 为 [162, 0] 或无法解析)');
}
break; break;
default: default:
Logger.warning('lxm--未处理的命令ID: $commandId,数据: $data'); Logger.warning('lxm--未处理的命令ID: $commandId,数据: $data');
@ -1091,6 +1168,11 @@ class BleManager extends GetxService {
List<String>? uuids, List<String>? uuids,
}) async { }) async {
try { try {
// 在注册新设备前先清除旧设备信息
if (macAddress != null && macAddress != currentDeviceAddress) {
clearDeviceInfo(); // 清除上一个设备的信息
}
// 保存设备地址信息 // 保存设备地址信息
if (macAddress != null) { if (macAddress != null) {
currentDeviceAddress = macAddress; currentDeviceAddress = macAddress;

4
lib/data/services/deapsound_ai_service.dart

@ -78,6 +78,10 @@ class DeapsoundAIService implements AiService {
..body = jsonEncode(requestBody); ..body = jsonEncode(requestBody);
final response = await client.send(request); final response = await client.send(request);
if (response.statusCode != 200) {
throw Exception('Unknown error');
}
await for (final chunk in response.stream await for (final chunk in response.stream
.transform(utf8.decoder) .transform(utf8.decoder)
.transform(const LineSplitter())) { .transform(const LineSplitter())) {

215
lib/data/services/meeting/meeting_task_service.dart

@ -8,16 +8,16 @@ import 'package:http/http.dart' as http;
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import '../../../core/utils/synchrodata.dart'; import '../../../core/utils/synchrodata.dart';
import '../db/sqflite_api.dart'; import '../db/sqflite_api.dart';
import '../deapsound_ai_service.dart';
import 'meeting_upload_service.dart'; import 'meeting_upload_service.dart';
import '../usage_stats_service.dart'; import '../../../data/models/appconfig.dart';
import '../../models/meeting_usage_stats.dart'; import 'summary_task_service.dart';
///会议任务服务类,负责管理会议音频的转写任务处理流程,包括任务提交、状态轮询、结果处理和AI分析等功能。 ///会议任务服务类,负责管理会议音频的转写任务处理流程,包括任务提交、状态轮询、结果处理和AI分析等功能。
class MeetingTaskService extends GetxService { class MeetingTaskService extends GetxService {
final GetStorage _storage = GetStorage(); final GetStorage _storage = GetStorage();
MeetingUploadService get _uploadService => Get.find<MeetingUploadService>(); MeetingUploadService get _uploadService => Get.find<MeetingUploadService>();
SummaryTaskService get _summaryTaskService => Get.find<SummaryTaskService>();
List _taskList = []; //待处理的会议任务列表 List _taskList = []; //待处理的会议任务列表
RxList taskFinishList = [].obs; //已处理的会议任务列表 RxList taskFinishList = [].obs; //已处理的会议任务列表
@ -26,42 +26,84 @@ class MeetingTaskService extends GetxService {
String _userId = ''; String _userId = '';
// final String _serviceUrl = 'https://openspeech.bytedance.com/api/v1/auc'; //
// final String _cluster = 'volc_auc_video_flash';
//字节跳动(火山引擎)语音服务配置 //字节跳动(火山引擎)语音服务配置
final String _appid = '2316081933'; // final String _appid = '2316081933';
final String _token = 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En'; // final String _token = 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En';
final String _cluster = 'volc_auc_video_flash'; // final String _cluster1 = 'volc.bigasr.auc';
final String _cluster1 = 'volc.bigasr.auc';
final String _serviceUrl = 'https://openspeech.bytedance.com/api/v1/auc'; // // final String _serviceUrl1 =
final String _serviceUrl1 = // 'https://openspeech.bytedance.com/api/v3/auc/bigmodel'; //大模型录音文件识别api地址
'https://openspeech.bytedance.com/api/v3/auc/bigmodel'; //大模型录音文件识别api地址 late final String _appid;
late final String _token;
late final String _cluster1;
late final String _serviceUrl1;
late final String _modelName;
late final String _modelVersion;
Map<String, String> _headers = {}; Map<String, String> _headers = {};
Map<String, String> _headers1 = {}; Map<String, String> _headers1 = {};
int meetingId = 0; int meetingId = 0;
RxInt tasktype = 1.obs; //任务类型,1-转写中,2-总结中,3-完成 RxInt tasktype = 1.obs; //任务类型,1-转写中,2-总结中,3-完成
List textList = []; //转写结果列表 List textList = []; //转写结果列表
String summary = ''; //AI总结内容
String overview = ''; //AI概览内容
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
_headers = {
'Authorization': 'Bearer; $_token', _initialize();
'Content-Type': 'application/json',
};
_headers1 = {
'X-Api-App-Key': _appid,
'X-Api-Access-Key': _token,
'X-Api-Resource-Id': _cluster1,
'X-Api-Request-Id': '',
'X-Api-Sequence': '-1',
};
_getAgent(); _getAgent();
_getTaskList(); _getTaskList();
} }
void cancelTask() {
_isTask = false;
_taskList.clear();
_storage.remove("meeting_task_list");
_storage.remove("meeting_task_finish_list");
_uploadService.cancelUpload();
_summaryTaskService.cancelTask();
}
Future<bool> _initialize() async {
try {
_appid = AppConfig.env('VOLC_OPENSPEECH_APP_ID') ?? '';
_token = AppConfig.env('VOLC_OPENSPEECH_ACCESS_TOKEN') ?? '';
_cluster1 = AppConfig.env('VOLC_OPENSPEECH_CLUSTER_BIGMODEL') ?? '';
_serviceUrl1 = AppConfig.env('VOLC_OPENSPEECH_BASE_URL_BIGMODEL') ?? '';
_modelName = AppConfig.env('VOLC_OPENSPEECH_MODEL_NAME') ?? 'bigmodel';
_modelVersion = AppConfig.env('VOLC_OPENSPEECH_MODEL_VERSION') ?? '400';
if (_appid.isEmpty ||
_token.isEmpty ||
_cluster1.isEmpty ||
_serviceUrl1.isEmpty ||
_modelName.isEmpty ||
_modelVersion.isEmpty) {
Logger.error('初始化会议任务服务失败:配置参数为空');
return false;
}
_headers = {
'Authorization': 'Bearer; $_token',
'Content-Type': 'application/json',
};
_headers1 = {
'X-Api-App-Key': _appid,
'X-Api-Access-Key': _token,
'X-Api-Resource-Id': _cluster1,
'X-Api-Request-Id': '',
'X-Api-Sequence': '-1',
};
} catch (e) {
Logger.error('初始化会议任务服务失败:$e');
return false;
}
return true;
}
void _getAgent() async { void _getAgent() async {
Map userInfo = _storage.read("user_info"); Map userInfo = _storage.read("user_info");
_userId = userInfo['user']['uid']; _userId = userInfo['user']['uid'];
@ -188,6 +230,7 @@ class MeetingTaskService extends GetxService {
if (isDeleteTask) { if (isDeleteTask) {
_taskList.removeWhere((element) => element['id'] == id); _taskList.removeWhere((element) => element['id'] == id);
_storage.write("meeting_task_list", _taskList); _storage.write("meeting_task_list", _taskList);
_summaryTaskService.removeTask(id);
} }
} }
@ -210,8 +253,8 @@ class MeetingTaskService extends GetxService {
"format": format, "format": format,
}, },
"request": { "request": {
"model_name": "bigmodel", // 模型名称,目前只有bigmodel "model_name": _modelName, // 模型名称,目前只有bigmodel
"model_version": "400", // 模型版本,400模型性能略有提升,且ITN有较大优化 "model_version": _modelVersion, // 模型版本,400模型性能略有提升,且ITN有较大优化
//"enable_itn": true, // 启用文本规范化,默认为true //"enable_itn": true, // 启用文本规范化,默认为true
"enable_punc": true, // 启用标点,默认为false "enable_punc": true, // 启用标点,默认为false
//"enable_ddc": false, // 启用语义顺滑,默认为false //"enable_ddc": false, // 启用语义顺滑,默认为false
@ -226,24 +269,24 @@ class MeetingTaskService extends GetxService {
Logger.info("lxm--大模型asr请求体$requestBody1"); Logger.info("lxm--大模型asr请求体$requestBody1");
final requestBody = jsonEncode({ // final requestBody = jsonEncode({
"app": { // "app": {
"appid": _appid, // "appid": _appid,
"token": _token, // "token": _token,
"cluster": _cluster, // "cluster": _cluster,
}, // },
"user": { // "user": {
"uid": _userId, // "uid": _userId,
}, // },
"audio": { // "audio": {
"format": format, // "format": format,
"url": audiourl, // "url": audiourl,
}, // },
"additions": { // "additions": {
"language": language, // "language": language,
"with_speaker_info": isSpeaker ? "True" : "False", // "with_speaker_info": isSpeaker ? "True" : "False",
} // }
}); // });
try { try {
final response = await http.post( final response = await http.post(
@ -280,12 +323,12 @@ class MeetingTaskService extends GetxService {
///轮询语音转文字任务状态 ///轮询语音转文字任务状态
///[taskId] 任务ID ///[taskId] 任务ID
Future<Map<String, dynamic>> _queryTask(String taskId) async { Future<Map<String, dynamic>> _queryTask(String taskId) async {
final queryBody = jsonEncode({ // final queryBody = jsonEncode({
"appid": _appid, // "appid": _appid,
"token": _token, // "token": _token,
"id": taskId, // "id": taskId,
"cluster": _cluster, // "cluster": _cluster,
}); // });
try { try {
_headers1['X-Api-Request-Id'] = taskId; _headers1['X-Api-Request-Id'] = taskId;
@ -323,7 +366,8 @@ class MeetingTaskService extends GetxService {
Logger.info("lxm---大模型asr响应体结果$respDic['result']['text']"); Logger.info("lxm---大模型asr响应体结果$respDic['result']['text']");
if (respDic['result']['text'] != null) { if (respDic['result']['text'] != null &&
respDic['result']['text'] != '') {
await _addTextList( await _addTextList(
item['id'], //会议ID item['id'], //会议ID
respDic['result']['utterances'], //语音转文字结果 respDic['result']['utterances'], //语音转文字结果
@ -412,75 +456,6 @@ class MeetingTaskService extends GetxService {
} catch (_) {} } catch (_) {}
// 触发AI分析,分别请求总结和概览 // 触发AI分析,分别请求总结和概览
aiServiceMessage(id, text, prompt, outline); _summaryTaskService.addTaskList(id, text, prompt, outline);
}
///触发AI分析,分别请求总结和概览
///[id] 会议ID
///[text] 语音转文字结果
///[prompt] AI总结提示词
///[outline] AI概览提示词
void aiServiceMessage(int id, String text, String prompt, String outline) {
int awaitQuantity = 0;
String summaryText = '';
String overviewText = '';
DeapsoundAIService().sendMessage(
messages: [
{
'role': 'system',
'content': prompt,
},
{'role': 'user', 'content': text},
],
).then((value) {
awaitQuantity++;
summaryText = value;
Logger.info("lxm---大模型asr总结结果$summaryText");
if (awaitQuantity >= 2) {
_aiServiceValue(id, summaryText, overviewText);
}
});
DeapsoundAIService().sendMessage(
messages: [
{
'role': 'system',
'content': outline,
},
{'role': 'user', 'content': text},
],
).then((value) {
awaitQuantity++;
overviewText = value;
Logger.info("lxm---大模型asr概览结果$overviewText");
if (awaitQuantity >= 2) {
_aiServiceValue(id, summaryText, overviewText);
}
});
}
///处理AI分析完成后的结果,包括数据库存储和状态更新
///[id] 会议ID
///[summaryText] 总结结果
///[overviewText] 概览结果
void _aiServiceValue(int id, String summaryText, String overviewText) {
SqfliteApi.editMeeting(id, {
'tasktype': 3,
'summary': summaryText,
'overview': overviewText,
});
if (id == meetingId) {
summary = summaryText;
overview = overviewText;
tasktype.value = 3;
}
Map taskFinishData = taskFinishList.firstWhere(
(element) => element['id'] == id,
orElse: () => {},
);
if (taskFinishData.isNotEmpty) {
taskFinishData['tasktype'] = 3;
taskFinishList.refresh();
}
_storage.write("meeting_task_finish_list", taskFinishList);
} }
} }

7
lib/data/services/meeting/meeting_upload_service.dart

@ -24,6 +24,13 @@ class MeetingUploadService extends GetxService {
_getUploadList(); _getUploadList();
} }
void cancelUpload() {
_isUpload = false;
_handled = 0;
uploadList.clear();
_storage.remove("meeting_upload_list");
}
void _getUploadList() { void _getUploadList() {
uploadList.value = _storage.read("meeting_upload_list") ?? []; uploadList.value = _storage.read("meeting_upload_list") ?? [];
executeUpload(); executeUpload();

160
lib/data/services/meeting/summary_task_service.dart

@ -0,0 +1,160 @@
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../../../core/utils/logger.dart';
import '../db/sqflite_api.dart';
import '../deapsound_ai_service.dart';
import 'meeting_task_service.dart';
class SummaryTaskService extends GetxService {
final _taskService = Get.find<MeetingTaskService>();
final GetStorage _storage = GetStorage();
List _taskList = [];
bool _isTask = false;
String summary = ''; //AI总结内容
String overview = ''; //AI概览内容
@override
void onInit() {
super.onInit();
_getTaskList();
}
void cancelTask() {
_taskList.clear();
_storage.remove("summary_task_list");
}
void _getTaskList() {
_taskList = _storage.read("summary_task_list") ?? [];
_executeTask();
}
Future<void> addTaskList(
int id, String text, String prompt, String outline) async {
Logger.debug("添加总结任务列表");
_taskList.add({
'id': id,
'text': text,
'prompt': prompt,
'outline': outline,
});
_storage.write("summary_task_list", _taskList);
_executeTask();
}
void _executeTask() async {
if (_taskList.isEmpty) return;
if (_isTask) return;
_isTask = true;
var task = _taskList.first;
_aiServiceMessage(
task['id'],
task['text'],
task['prompt'],
task['outline'],
);
Logger.debug("执行总结任务列表");
}
void removeTask(int id) {
_taskList.removeWhere((element) => element['id'] == id);
_storage.write("summary_task_list", _taskList);
}
void _aiServiceMessage(int id, String text, String prompt, String outline) {
final ai = DeapsoundAIService();
String summaryText = '';
String overviewText = '';
var summaryDone = false;
var overviewDone = false;
var summarySuccess = false;
var overviewSuccess = false;
void tryFinish() {
if (summaryDone && overviewDone) {
if (summarySuccess &&
overviewSuccess &&
summaryText.isNotEmpty &&
overviewText.isNotEmpty) {
_aiServiceValue(id, summaryText, overviewText);
} else {
_isTask = false;
_executeTask();
}
}
}
final summaryStream = ai.sendMessageStream(messages: [
{'role': 'system', 'content': prompt},
{'role': 'user', 'content': text},
]).timeout(const Duration(seconds: 120));
final overviewStream = ai.sendMessageStream(messages: [
{'role': 'system', 'content': outline},
{'role': 'user', 'content': text},
]).timeout(const Duration(seconds: 120));
summaryStream.listen((chunk) {
summaryText += chunk;
}, onError: (error) {
summaryDone = true;
summarySuccess = false;
tryFinish();
}, onDone: () {
summaryDone = true;
summarySuccess = true;
tryFinish();
}, cancelOnError: true);
overviewStream.listen((chunk) {
overviewText += chunk;
}, onError: (error) {
overviewDone = true;
overviewSuccess = false;
tryFinish();
}, onDone: () {
overviewDone = true;
overviewSuccess = true;
tryFinish();
}, cancelOnError: true);
}
Future<void> _aiServiceValue(
int id, String summaryText, String overviewText) async {
final exists = _taskList.any((item) => item['id'] == id);
if (!exists) {
_isTask = false;
_executeTask();
return;
}
await SqfliteApi.editMeeting(id, {
'tasktype': 3,
'summary': summaryText,
'overview': overviewText,
});
if (id == _taskService.meetingId) {
summary = summaryText;
overview = overviewText;
_taskService.tasktype.value = 3;
}
Map taskFinishData = _taskService.taskFinishList.firstWhere(
(element) => element['id'] == id,
orElse: () => {},
);
if (taskFinishData.isNotEmpty) {
taskFinishData['tasktype'] = 3;
_taskService.taskFinishList.refresh();
}
_storage.write("meeting_task_finish_list", _taskService.taskFinishList);
_taskList.removeWhere((element) => element['id'] == id);
_storage.write("summary_task_list", _taskList);
_isTask = false;
_executeTask();
Logger.debug("总结任务完成");
}
}

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;
} }
} }

10
lib/data/services/pay_service.dart

@ -1,13 +1,5 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'dart:convert'; import '/data/services/network/api.dart';
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:in_app_purchase_android/in_app_purchase_android.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../../data/services/network/api.dart';
import 'package:tobias/tobias.dart'; import 'package:tobias/tobias.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../data/models/user_Info.dart'; import '../../data/models/user_Info.dart';

545
lib/data/services/translation_usage_service.dart

@ -0,0 +1,545 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../models/translation_usage_stats.dart';
import '../../core/utils/logger.dart';
/// 翻译使用量统计服务
class TranslationUsageService extends GetxService {
static const String _storageKey = 'translation_usage_stats';
static const String _dailyStatsKey = 'daily_translation_stats';
static const String _costConfigKey = 'translation_cost_config';
final GetStorage _storage = GetStorage();
// 实时统计数据
final Rx<RealtimeUsageStats> realtimeStats = RealtimeUsageStats().obs;
// 当日统计数据
final Rx<TranslationUsageStats> todayStats =
TranslationUsageStats.empty('').obs;
// 成本配置
final RxMap<String, double> costConfig = <String, double>{}.obs;
// 预警阈值
final RxDouble dailyCostThreshold = 50.0.obs; // 日成本预警阈值
final RxDouble monthlyCostThreshold = 300.0.obs; // 月成本预警阈值
@override
void onInit() {
super.onInit();
_initializeService();
}
/// 初始化服务
void _initializeService() {
_loadCostConfig();
_loadTodayStats();
Logger.info('翻译使用量统计服务初始化完成');
}
/// 加载成本配置
void _loadCostConfig() {
try {
final configData = _storage.read(_costConfigKey);
if (configData != null) {
costConfig.value = Map<String, double>.from(configData);
} else {
// 设置默认成本配置(每千字符的成本,单位:元)
costConfig.value = {
'translation_per_1k_chars': 0.05, // 翻译:每千字符0.05元
'asr_per_minute': 0.02, // ASR:每分钟0.02元
'tts_per_1k_chars': 0.03, // TTS:每千字符0.03元
'ai_summary_per_request': 0.01, // AI摘要:每次请求0.01元
};
_saveCostConfig();
}
} catch (e) {
Logger.error('加载成本配置失败: $e');
}
}
/// 保存成本配置
void _saveCostConfig() {
try {
_storage.write(_costConfigKey, costConfig.value);
} catch (e) {
Logger.error('保存成本配置失败: $e');
}
}
/// 加载当日统计数据
void _loadTodayStats() {
try {
final today = DateTime.now();
final todayKey =
'${today.year}-${today.month.toString().padLeft(2, '0')}-${today.day.toString().padLeft(2, '0')}';
final statsData = _storage.read('${_dailyStatsKey}_$todayKey');
if (statsData != null) {
todayStats.value = TranslationUsageStats.fromJson(
Map<String, dynamic>.from(statsData));
} else {
todayStats.value = TranslationUsageStats.empty(getCurrentUserId());
}
} catch (e) {
Logger.error('加载当日统计数据失败: $e');
todayStats.value = TranslationUsageStats.empty(getCurrentUserId());
}
}
/// 获取当前用户ID
String getCurrentUserId() {
try {
final userInfo = _storage.read('user_info');
if (userInfo != null && userInfo['user'] != null) {
return userInfo['user']['uid'] ?? 'anonymous';
}
} catch (e) {
Logger.warning('获取用户ID失败: $e');
}
return 'anonymous';
}
/// 记录翻译API调用
void recordTranslationApiCall({
required String sourceText,
required String targetText,
required String sourceLanguage,
required String targetLanguage,
required String mode,
}) {
try {
// 更新实时统计
realtimeStats.value.translationCount++;
realtimeStats.value.sourceTextCharacters += sourceText.length;
realtimeStats.value.ttsTextCharacters += targetText.length;
realtimeStats.value.currentLanguagePair =
'$sourceLanguage-$targetLanguage';
realtimeStats.value.currentMode = mode;
realtimeStats.refresh();
// 更新当日统计 - 使用多维度统计
final languagePair = '$sourceLanguage→$targetLanguage';
// 更新语言对统计
final newLanguagePairStats = Map<String, LanguagePairStats>.from(
todayStats.value.languagePairStats);
if (newLanguagePairStats.containsKey(languagePair)) {
final existing = newLanguagePairStats[languagePair]!;
newLanguagePairStats[languagePair] = LanguagePairStats(
languagePair: languagePair,
translationCount: existing.translationCount + 1,
sourceCharacters: existing.sourceCharacters + sourceText.length,
cost: existing.cost + _calculateTranslationCost(sourceText.length),
);
} else {
newLanguagePairStats[languagePair] = LanguagePairStats(
languagePair: languagePair,
translationCount: 1,
sourceCharacters: sourceText.length,
cost: _calculateTranslationCost(sourceText.length),
);
}
// 更新模式统计
final newModeStats =
Map<String, TranslationModeStats>.from(todayStats.value.modeStats);
if (newModeStats.containsKey(mode)) {
final existing = newModeStats[mode]!;
newModeStats[mode] = existing.copyWith(
apiCalls: existing.apiCalls + 1,
sourceCharacters: existing.sourceCharacters + sourceText.length,
ttsCharacters: existing.ttsCharacters + targetText.length,
cost: existing.cost + _calculateTranslationCost(sourceText.length),
);
} else {
newModeStats[mode] = TranslationModeStats(
apiCalls: 1,
sourceCharacters: sourceText.length,
ttsCharacters: targetText.length,
cost: _calculateTranslationCost(sourceText.length),
);
}
todayStats.value = todayStats.value.copyWith(
translationApiCalls: todayStats.value.translationApiCalls + 1,
sourceTextCharacters:
todayStats.value.sourceTextCharacters + sourceText.length,
ttsTextCharacters:
todayStats.value.ttsTextCharacters + targetText.length,
languagePairStats: newLanguagePairStats,
modeStats: newModeStats,
estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith(
translationApiCalls: todayStats.value.translationApiCalls + 1,
sourceTextCharacters:
todayStats.value.sourceTextCharacters + sourceText.length,
ttsTextCharacters:
todayStats.value.ttsTextCharacters + targetText.length,
)),
);
_saveTodayStats();
_checkCostThreshold();
Logger.info(
'记录翻译API调用: 源文本${sourceText.length}字符, 目标文本${targetText.length}字符');
} catch (e) {
Logger.error('记录翻译API调用失败: $e');
}
}
/// 记录ASR API调用 - 优化版(精确计费参数)
void recordAsrApiCall({
required int effectiveDurationSeconds, // 有效语音时长(VAD检测的实际语音)
required int totalDurationSeconds, // 总会话时长(包含静音)
required String language,
required String mode, // 翻译模式
String? audioFormat, // 音频格式(可选)
int? sampleRate, // 采样率(可选)
}) {
try {
// 更新实时统计(使用有效语音时长作为主要计费依据)
realtimeStats.value.effectiveAudioDuration += effectiveDurationSeconds;
realtimeStats.value.totalSessionDuration += totalDurationSeconds;
realtimeStats.refresh();
// 更新模式统计
final newModeStats =
Map<String, TranslationModeStats>.from(todayStats.value.modeStats);
if (newModeStats.containsKey(mode)) {
final existing = newModeStats[mode]!;
newModeStats[mode] = existing.copyWith(
effectiveAudioDuration:
existing.effectiveAudioDuration + effectiveDurationSeconds,
cost: existing.cost + _calculateAsrCost(effectiveDurationSeconds),
);
} else {
newModeStats[mode] = TranslationModeStats(
effectiveAudioDuration: effectiveDurationSeconds,
cost: _calculateAsrCost(effectiveDurationSeconds),
);
}
// 更新当日统计
todayStats.value = todayStats.value.copyWith(
asrApiCalls: todayStats.value.asrApiCalls + 1,
effectiveAudioDuration:
todayStats.value.effectiveAudioDuration + effectiveDurationSeconds,
totalSessionDuration:
todayStats.value.totalSessionDuration + totalDurationSeconds,
modeStats: newModeStats,
estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith(
asrApiCalls: todayStats.value.asrApiCalls + 1,
effectiveAudioDuration: todayStats.value.effectiveAudioDuration +
effectiveDurationSeconds,
)),
);
_saveTodayStats();
Logger.info(
'ASR统计: 有效语音${effectiveDurationSeconds}秒, 总时长${totalDurationSeconds}秒, 语言$language, 模式$mode');
} catch (e) {
Logger.error('记录ASR API调用失败: $e');
}
}
/// 记录TTS API调用
void recordTtsApiCall({
required String text,
required int durationSeconds,
required String language,
}) {
try {
// 更新实时统计
realtimeStats.value.totalSessionDuration += durationSeconds;
realtimeStats.refresh();
// 更新当日统计(TTS按字符数计费,不是按时长)
todayStats.value = todayStats.value.copyWith(
ttsApiCalls: todayStats.value.ttsApiCalls + 1,
ttsTextCharacters: todayStats.value.ttsTextCharacters + text.length,
totalSessionDuration:
todayStats.value.totalSessionDuration + durationSeconds,
estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith(
ttsApiCalls: todayStats.value.ttsApiCalls + 1,
ttsTextCharacters: todayStats.value.ttsTextCharacters + text.length,
)),
);
_saveTodayStats();
Logger.info('记录TTS API调用: 文本${text.length}字符, 时长${durationSeconds}秒');
} catch (e) {
Logger.error('记录TTS API调用失败: $e');
}
}
/// 记录AI摘要API调用
void recordAiSummaryApiCall() {
try {
// 更新当日统计
todayStats.value = todayStats.value.copyWith(
aiSummaryApiCalls: todayStats.value.aiSummaryApiCalls + 1,
estimatedCost: _calculateEstimatedCost(todayStats.value.copyWith(
aiSummaryApiCalls: todayStats.value.aiSummaryApiCalls + 1,
)),
);
_saveTodayStats();
Logger.info('记录AI摘要API调用');
} catch (e) {
Logger.error('记录AI摘要API调用失败: $e');
}
}
/// 开始新会话
void startNewSession() {
realtimeStats.value.reset();
// 更新当日会话数
todayStats.value = todayStats.value.copyWith(
sessionCount: todayStats.value.sessionCount + 1,
);
_saveTodayStats();
Logger.info('开始新的翻译会话');
}
/// 计算预估成本 - 优化版(基于精确计费参数)
double _calculateEstimatedCost(TranslationUsageStats stats) {
double cost = 0.0;
// 翻译成本:基于源文本字符数
cost += (stats.sourceTextCharacters / 1000) *
(costConfig['translation_per_1k_chars'] ?? 0.05);
// ASR成本:基于有效音频时长
cost += (stats.effectiveAudioDuration / 60) *
(costConfig['asr_per_minute'] ?? 0.02);
// TTS成本:基于TTS文本字符数
cost += (stats.ttsTextCharacters / 1000) *
(costConfig['tts_per_1k_chars'] ?? 0.03);
// AI摘要成本(已废弃但保留兼容性)
cost += stats.aiSummaryApiCalls *
(costConfig['ai_summary_per_request'] ?? 0.01);
return cost;
}
/// 计算翻译成本(单次调用)
double _calculateTranslationCost(int sourceCharacters) {
return (sourceCharacters / 1000) *
(costConfig['translation_per_1k_chars'] ?? 0.05);
}
/// 计算ASR成本(单次调用)
double _calculateAsrCost(int durationSeconds) {
return (durationSeconds / 60) * (costConfig['asr_per_minute'] ?? 0.02);
}
/// 计算TTS成本(单次调用)
double _calculateTtsCost(int textCharacters) {
return (textCharacters / 1000) * (costConfig['tts_per_1k_chars'] ?? 0.03);
}
/// 保存当日统计数据
void _saveTodayStats() {
try {
final today = DateTime.now();
final todayKey =
'${today.year}-${today.month.toString().padLeft(2, '0')}-${today.day.toString().padLeft(2, '0')}';
_storage.write('${_dailyStatsKey}_$todayKey', todayStats.value.toJson());
} catch (e) {
Logger.error('保存当日统计数据失败: $e');
}
}
/// 检查成本阈值
void _checkCostThreshold() {
final todayCost = todayStats.value.estimatedCost;
if (todayCost >= dailyCostThreshold.value) {
_showCostWarning('今日翻译成本已达到预警阈值:¥${todayCost.toStringAsFixed(2)}');
}
}
/// 显示成本预警
void _showCostWarning(String message) {
Get.snackbar(
'成本预警',
message,
snackPosition: SnackPosition.TOP,
backgroundColor: Get.theme.colorScheme.error.withOpacity(0.1),
colorText: Get.theme.colorScheme.error,
duration: const Duration(seconds: 5),
);
}
/// 获取历史统计数据
Future<List<TranslationUsageStats>> getHistoryStats({
required DateTime startDate,
required DateTime endDate,
}) async {
try {
final List<TranslationUsageStats> historyStats = [];
for (DateTime date = startDate;
date.isBefore(endDate) || date.isAtSameMomentAs(endDate);
date = date.add(const Duration(days: 1))) {
final dateKey =
'${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
final statsData = _storage.read('${_dailyStatsKey}_$dateKey');
if (statsData != null) {
historyStats.add(TranslationUsageStats.fromJson(
Map<String, dynamic>.from(statsData)));
}
}
return historyStats;
} catch (e) {
Logger.error('获取历史统计数据失败: $e');
return [];
}
}
/// 清理过期数据(保留最近90天)
void cleanupOldData() {
try {
final cutoffDate = DateTime.now().subtract(const Duration(days: 90));
final allKeys = _storage.getKeys();
for (final key in allKeys) {
if (key.startsWith(_dailyStatsKey)) {
final dateStr = key.replaceFirst('${_dailyStatsKey}_', '');
try {
final date = DateTime.parse(dateStr);
if (date.isBefore(cutoffDate)) {
_storage.remove(key);
Logger.info('清理过期统计数据: $key');
}
} catch (e) {
// 忽略解析错误的键
}
}
}
} catch (e) {
Logger.error('清理过期数据失败: $e');
}
}
/// 更新成本配置
void updateCostConfig(Map<String, double> newConfig) {
costConfig.value = newConfig;
_saveCostConfig();
// 重新计算当日成本
todayStats.value = todayStats.value.copyWith(
estimatedCost: _calculateEstimatedCost(todayStats.value),
);
_saveTodayStats();
Logger.info('更新成本配置完成');
}
/// 更新预警阈值
void updateThresholds({
double? dailyThreshold,
double? monthlyThreshold,
}) {
if (dailyThreshold != null) {
dailyCostThreshold.value = dailyThreshold;
}
if (monthlyThreshold != null) {
monthlyCostThreshold.value = monthlyThreshold;
}
// 保存阈值配置
_storage.write('daily_cost_threshold', dailyCostThreshold.value);
_storage.write('monthly_cost_threshold', monthlyCostThreshold.value);
Logger.info('更新预警阈值完成');
}
/// 获取月度统计汇总
Future<TranslationUsageStats> getMonthlyStats(int year, int month) async {
try {
final startDate = DateTime(year, month, 1);
final endDate = DateTime(year, month + 1, 0); // 月末
final dailyStats = await getHistoryStats(
startDate: startDate,
endDate: endDate,
);
if (dailyStats.isEmpty) {
return TranslationUsageStats.empty(getCurrentUserId());
}
// 汇总月度数据
int totalTranslationCalls = 0;
int totalAsrCalls = 0;
int totalTtsCalls = 0;
int totalAiSummaryCalls = 0;
int totalSourceChars = 0;
int totalTargetChars = 0;
int totalRecordingDuration = 0;
int totalTtsPlayDuration = 0;
int totalSessions = 0;
double totalCost = 0.0;
final Map<String, int> combinedLanguagePairUsage = {};
final Map<String, int> combinedModeUsage = {};
for (final stats in dailyStats) {
totalTranslationCalls += stats.translationApiCalls;
totalAsrCalls += stats.asrApiCalls;
totalTtsCalls += stats.ttsApiCalls;
totalAiSummaryCalls += stats.aiSummaryApiCalls;
totalSourceChars += stats.sourceCharacterCount;
totalTargetChars += stats.targetCharacterCount;
totalRecordingDuration += stats.audioRecordingDuration;
totalTtsPlayDuration += stats.ttsPlaybackDuration;
totalSessions += stats.sessionCount;
totalCost += stats.estimatedCost;
// 合并语言对使用统计
stats.languagePairUsage.forEach((key, value) {
combinedLanguagePairUsage[key] =
(combinedLanguagePairUsage[key] ?? 0) + value;
});
// 合并模式使用统计
stats.modeUsage.forEach((key, value) {
combinedModeUsage[key] = (combinedModeUsage[key] ?? 0) + value;
});
}
return TranslationUsageStats(
id: 'monthly_${year}_$month',
date: startDate,
userId: getCurrentUserId(),
timeGranularity: 'month',
translationApiCalls: totalTranslationCalls,
asrApiCalls: totalAsrCalls,
ttsApiCalls: totalTtsCalls,
aiSummaryApiCalls: totalAiSummaryCalls,
sourceTextCharacters: totalSourceChars,
effectiveAudioDuration: totalRecordingDuration,
ttsTextCharacters: totalTargetChars,
totalSessionDuration: totalTtsPlayDuration,
sessionCount: totalSessions,
estimatedCost: totalCost,
);
} catch (e) {
Logger.error('获取月度统计失败: $e');
return TranslationUsageStats.empty(getCurrentUserId());
}
}
}

12
lib/modules/agent/views/message_bubble.dart

@ -1,9 +1,9 @@
import 'package:voitrans/core/utils/logger.dart'; import '/core/utils/logger.dart';
import 'package:voitrans/modules/agent/views/google_navigation_card.dart'; import '/modules/agent/views/google_navigation_card.dart';
import 'package:voitrans/modules/agent/views/music_card.dart'; import '/modules/agent/views/music_card.dart';
import 'package:voitrans/modules/agent/views/qqmusic_card.dart'; import '/modules/agent/views/qqmusic_card.dart';
import 'package:voitrans/modules/agent/views/weather_baidu.dart'; import '/modules/agent/views/weather_baidu.dart';
import 'package:voitrans/modules/agent/views/weather_hefeng.dart'; import '/modules/agent/views/weather_hefeng.dart';
import '../../../data/models/song_model.dart'; import '../../../data/models/song_model.dart';
import '../../../modules/agent/views/navigation_card.dart'; import '../../../modules/agent/views/navigation_card.dart';

2
lib/modules/agent/views/qqmusic_card.dart

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:voitrans/modules/agent/widgets/circular_progress_button.dart'; import '/modules/agent/widgets/circular_progress_button.dart';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
import '../../../data/services/qq_music.dart'; import '../../../data/services/qq_music.dart';
import '../../../data/models/song_model.dart'; import '../../../data/models/song_model.dart';

2
lib/modules/devices/controllers/devices_controller.dart

@ -1,6 +1,6 @@
// import 'dart:ffi'; // Not supported on web; not needed here // import 'dart:ffi'; // Not supported on web; not needed here
import 'package:voitrans/data/models/appconfig_model.dart'; import '/data/models/appconfig_model.dart';
import '/core/utils/logger.dart'; import '/core/utils/logger.dart';
import '/data/models/user_Info.dart'; import '/data/models/user_Info.dart';

171
lib/modules/devices/views/devices_view.dart

@ -1,5 +1,6 @@
import 'dart:ui'; import 'dart:ui';
import '../../../routes/app_routes.dart';
import '/modules/devices/controllers/devices_controller.dart'; import '/modules/devices/controllers/devices_controller.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
@ -201,103 +202,107 @@ class DevicesView extends GetView<DevicesController> {
required DeviceType deviceType, required DeviceType deviceType,
required bool isDarkMode, required bool isDarkMode,
}) { }) {
return Container( return GestureDetector(
margin: EdgeInsets.only(bottom: 12.h), onTap: () => Get.toNamed(Routes.eqSettings),
padding: EdgeInsets.all(16.w), child: Container(
decoration: BoxDecoration( margin: EdgeInsets.only(bottom: 12.h),
color: isDarkMode ? AppColors.cardBackground : Colors.white, padding: EdgeInsets.all(16.w),
borderRadius: BorderRadius.circular(12.r), decoration: BoxDecoration(
boxShadow: [ color: isDarkMode ? AppColors.cardBackground : Colors.white,
BoxShadow( borderRadius: BorderRadius.circular(12.r),
color: Colors.black.withOpacity(0.05), boxShadow: [
blurRadius: 8, BoxShadow(
offset: const Offset(0, 2), color: Colors.black.withOpacity(0.05),
), blurRadius: 8,
], offset: const Offset(0, 2),
),
child: Row(
children: [
// 设备图标
Container(
width: 48.w,
height: 48.w,
decoration: BoxDecoration(
color:
isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[100],
borderRadius: BorderRadius.circular(12.r),
), ),
child: Icon( ],
deviceType.icon, ),
color: isDarkMode ? Colors.white : Colors.black87, child: Row(
size: 24.sp, children: [
// 设备图标
Container(
width: 48.w,
height: 48.w,
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[100],
borderRadius: BorderRadius.circular(12.r),
),
child: Icon(
deviceType.icon,
color: isDarkMode ? Colors.white : Colors.black87,
size: 24.sp,
),
), ),
), SizedBox(width: 16.w),
SizedBox(width: 16.w), // 设备信息
// 设备信息 Expanded(
Expanded( child: Column(
child: Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Text(
Text( device.devicename.isNotEmpty
device.devicename.isNotEmpty ? device.devicename
? device.devicename : "deviceTypeUnknown".tr,
: "deviceTypeUnknown".tr, style: TextStyle(
style: TextStyle( fontSize: 16.sp,
fontSize: 16.sp, fontWeight: FontWeight.w500,
fontWeight: FontWeight.w500, color: isDarkMode ? Colors.white : Colors.black87,
color: isDarkMode ? Colors.white : Colors.black87, ),
),
),
SizedBox(height: 4.h),
Text(
device.devicemac.isNotEmpty
? device.devicemac
: "noMacAddress".tr,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white70 : Colors.grey[600],
), ),
),
if (device.license.isNotEmpty) ...[
SizedBox(height: 4.h), SizedBox(height: 4.h),
Text( Text(
"${"licenseCode".tr}: ${device.license}", device.devicemac.isNotEmpty
? device.devicemac
: "noMacAddress".tr,
style: TextStyle( style: TextStyle(
fontSize: 12.sp, fontSize: 14.sp,
color: isDarkMode ? Colors.white54 : Colors.grey[500], color: isDarkMode ? Colors.white70 : Colors.grey[600],
), ),
), ),
if (device.license.isNotEmpty) ...[
SizedBox(height: 4.h),
Text(
"${"licenseCode".tr}: ${device.license}",
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white54 : Colors.grey[500],
),
),
],
], ],
],
),
),
// 解绑按钮
GestureDetector(
onTap: () => _showUnbindDialog(device, isDarkMode),
child: Container(
padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 6.h,
), ),
decoration: BoxDecoration( ),
color: Colors.red.withOpacity(0.1), // 解绑按钮
borderRadius: BorderRadius.circular(6.r), GestureDetector(
border: Border.all( onTap: () => _showUnbindDialog(device, isDarkMode),
color: Colors.red.withOpacity(0.3), child: Container(
width: 1, padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 6.h,
), ),
), decoration: BoxDecoration(
child: Text( color: Colors.red.withOpacity(0.1),
"unbind".tr, borderRadius: BorderRadius.circular(6.r),
style: TextStyle( border: Border.all(
fontSize: 12.sp, color: Colors.red.withOpacity(0.3),
color: Colors.red, width: 1,
fontWeight: FontWeight.w500, ),
),
child: Text(
"unbind".tr,
style: TextStyle(
fontSize: 12.sp,
color: Colors.red,
fontWeight: FontWeight.w500,
),
), ),
), ),
), ),
), ],
], ),
), ),
); );
} }

10
lib/modules/eq_settings/bindings/eq_settings_binding.dart

@ -0,0 +1,10 @@
import 'package:get/get.dart';
import '../controllers/eq_settings_controller.dart';
class EqSettingsBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<EqSettingsController>(() => EqSettingsController());
}
}

217
lib/modules/eq_settings/controllers/eq_settings_controller.dart

@ -0,0 +1,217 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../../../core/utils/logger.dart';
import '../../../routes/app_routes.dart';
import '../views/widgets/row_confirm_widget.dart';
import '../views/widgets/row_equalizer_widget.dart';
import '../views/widgets/row_grade_widget.dart';
import '../views/widgets/row_input_widget.dart';
import '../views/widgets/row_keypress_widget.dart';
import '../views/widgets/row_radio_widget.dart';
import '../views/widgets/row_switch_widget.dart';
import '../views/widgets/row_timer_widget.dart';
class EqSettingsController extends GetxController {
final RxList uiList = [
{
'title': '音效',
'templates': [
{
'type': 'equalizer',
'name': '均衡器',
'value': [],
'options': [
{
'label': '默认',
'value': [1, 2, 3, 4, 5, 4, 3, 2, 1, 0],
'default': true,
},
{
'label': '流行',
'value': [1, 0, 3, 4, 5, 4, 1, 2, 1, 0],
'default': true,
},
{
'label': '摇滚',
'value': [2, 3, 4, 5, 4, 3, 2, 1, 0, -1],
'default': true,
},
{
'label': '爵士',
'value': [3, 4, 0, 4, 3, 2, 1, 0, -1, -2],
'default': true,
},
{
'label': '经典',
'value': [3, 4, 2, 4, 3, 2, 1, 0, -1, -2],
'default': true,
},
{
'label': '乡村',
'value': [3, 4, 1, 4, 3, 2, 1, 0, -1, -2],
'default': true,
},
],
'xAxis': [
'8',
'31',
'62',
'125',
'250',
'500',
'1K',
'2K',
'4K',
'16K'
],
'onCallback': (List value) {
Logger.d('equalizer', '均衡器:$value');
},
}
],
},
{
'title': '按键功能',
'templates': [
{
'type': 'keypress',
'data': [
{'key': 'key1', 'label': '单击', 'left': 1, 'right': 1},
{'key': 'key2', 'label': '双击', 'left': 1, 'right': 1},
{'key': 'key3', 'label': '三击', 'left': 1, 'right': 1},
{'key': 'key4', 'label': '长按', 'left': 1, 'right': 1},
],
'options': [
{'label': '无作用', 'value': 0},
{'label': '回拨电话', 'value': 1},
{'label': '语音助手', 'value': 2},
{'label': '上一曲', 'value': 3},
{'label': '下一曲', 'value': 4},
{'label': '音量加', 'value': 5},
{'label': '音量减', 'value': 6},
{'label': '播放暂停', 'value': 7},
{'label': '游戏模式', 'value': 8},
{'label': '降噪设置', 'value': 9},
{'label': 'EQ切换', 'value': 10},
],
'onCallback': (Map value) {
Logger.d('keypress', '按键功能:$value');
},
},
],
},
{
'title': '耳机功能',
'templates': [
{
'type': 'input',
'name': '修改名称',
'value': 'voitrans',
'onCallback': (String value) {
Logger.d('input', '修改名称:$value');
},
},
{
'type': 'radio',
'name': '工作模式',
'value': 1,
'options': [
{'label': '游戏模式', 'value': 1},
{'label': '娱乐模式', 'value': 2},
{'label': '睡眠模式', 'value': 3},
],
'onCallback': (int value) {
Logger.d('radio', '工作模式:$value');
},
},
{
'type': 'switch',
'name': '闪灯开关',
'value': false,
'onCallback': (bool value) {
Logger.d('switch', '闪灯开关:$value');
},
},
{
'type': 'switch',
'name': '查找设备',
'value': false,
'description': '在设备上播放声音',
'onCallback': (bool value) {
Logger.d('switch', '查找设备:$value');
},
},
{
'type': 'grade',
'name': '降噪等级',
'value': 0,
'onCallback': (int value) {
Logger.d('grade', '降噪等级:$value');
},
},
{
'type': 'timer',
'name': '定时关机',
'value': 0,
'onCallback': (int value) {
Logger.d('timer', '定时关机:$value');
},
},
],
},
{
'templates': [
{
'type': 'confirm',
'name': '升级耳机固件',
'onCallback': () => Get.toNamed(Routes.OTA_UPGRADE),
},
{
'type': 'confirm',
'name': '恢复出厂设置',
'tips': '确定要恢复出厂设置吗?',
'onCallback': () {
Logger.d('confirm', '恢复出厂设置');
},
},
{
'type': 'confirm',
'name': '清除配对记录',
'tips': '确定要清除配对记录吗?',
'onCallback': () {
Logger.d('confirm', '清除配对记录');
},
},
],
}
].obs;
final GetStorage _storage = GetStorage();
@override
void onInit() {
super.onInit();
final List equalizerOptions = _storage.read('equalizer_options') ?? [];
if (equalizerOptions.isNotEmpty) {
List options =
equalizerOptions.map((e) => Map<String, Object>.from(e)).toList();
uiList[0]['templates'][0]['options'].addAll(options);
}
}
Widget getTemplate(Map data) {
return switch (data['type']) {
'equalizer' => RowEqualizerWidget(data),
'keypress' => RowKeypressWidget(data),
'input' => RowInputWidget(data),
'radio' => RowRadioWidget(data),
'switch' => RowSwitchWidget(data),
'grade' => RowGradeWidget(data),
'timer' => RowTimerWidget(data),
'confirm' => RowConfirmWidget(data),
_ => const SizedBox.shrink(),
};
}
}

95
lib/modules/eq_settings/views/eq_settings_view.dart

@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../controllers/eq_settings_controller.dart';
class EqSettingsView extends GetView<EqSettingsController> {
const EqSettingsView({super.key});
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: _buildAppBar(isDarkMode),
body: _buildBody(isDarkMode),
);
}
// 构建应用栏
PreferredSizeWidget _buildAppBar(bool isDarkMode) {
return AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
surfaceTintColor: Colors.transparent,
elevation: 0.5,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
onPressed: () => Get.back(),
),
title: Text(
'设备设置',
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
centerTitle: true,
);
}
// 构建主体内容
Widget _buildBody(bool isDarkMode) {
return Obx(
() => ListView.builder(
itemCount: controller.uiList.length,
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 5.w),
itemBuilder: (BuildContext context, int index) {
Map data = controller.uiList[index];
return _item(isDarkMode, data);
},
),
);
}
Widget _item(bool isDarkMode, Map data) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w),
margin: EdgeInsets.symmetric(vertical: 5.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withValues(alpha: 0.05) : Colors.white,
borderRadius: BorderRadius.circular(8.w),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withValues(alpha: 0.2)
: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (data['title'] != null)
Text(
data['title'],
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
...data['templates'].map((e) => controller.getTemplate(e)).toList(),
],
),
);
}
}

96
lib/modules/eq_settings/views/widgets/row_confirm_widget.dart

@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
class RowConfirmWidget extends StatefulWidget {
final Map templateData;
const RowConfirmWidget(this.templateData, {super.key});
@override
State<RowConfirmWidget> createState() => _RowConfirmWidgetState();
}
class _RowConfirmWidgetState extends State<RowConfirmWidget> {
Map get _templateData => widget.templateData;
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return GestureDetector(
onTap: () {
if (_templateData['tips'] != null) {
_alertDialog(isDarkMode);
} else {
_templateData['onCallback']();
}
},
behavior: HitTestBehavior.translucent,
child: Padding(
padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w),
child: Row(
children: [
Expanded(
child: Text(
_templateData['name'],
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
Icon(
Icons.chevron_right,
size: 26.w,
color: Colors.blue,
),
],
),
),
);
}
void _alertDialog(bool isDarkMode) {
Get.dialog(
AlertDialog(
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
title: Text(
_templateData['name'],
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
),
),
content: Text(
_templateData['tips'],
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr, // 取消
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
),
TextButton(
onPressed: () {
_templateData['onCallback']();
Get.back();
},
child: Text(
'confirm'.tr, // 确定
style: TextStyle(
color: isDarkMode ? Colors.red[300] : Colors.red[600],
),
),
),
],
),
);
}
}

83
lib/modules/eq_settings/views/widgets/row_equalizer_widget.dart

@ -0,0 +1,83 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'select_equalizer_widget.dart';
class RowEqualizerWidget extends StatefulWidget {
final Map templateData;
const RowEqualizerWidget(this.templateData, {super.key});
@override
State<RowEqualizerWidget> createState() => _RowEqualizerWidgetState();
}
class _RowEqualizerWidgetState extends State<RowEqualizerWidget> {
Map get _templateData => widget.templateData;
String _label = '';
@override
void initState() {
super.initState();
_label = _templateData['options'].firstWhere(
(option) => listEquals(_templateData['value'], option['value']),
orElse: () => {'label': ''},
)['label'];
}
void _setLabel() {
setState(() {
_label = _templateData['options'].firstWhere(
(option) => listEquals(_templateData['value'], option['value']),
orElse: () => {'label': ''},
)['label'];
});
}
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return GestureDetector(
onTap: () async {
var result = await Get.to(() => SelectEqualizerWidget(_templateData));
_setLabel();
if (result != null) {
_templateData['onCallback'](_templateData['value']);
}
},
behavior: HitTestBehavior.translucent,
child: Padding(
padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w),
child: Row(
children: [
Expanded(
child: Text(
_templateData['name'],
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
Text(
_label,
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white : Colors.grey,
),
),
Icon(
Icons.chevron_right,
size: 26.w,
color: Colors.blue,
),
],
),
),
);
}
}

63
lib/modules/eq_settings/views/widgets/row_grade_widget.dart

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class RowGradeWidget extends StatefulWidget {
final Map templateData;
const RowGradeWidget(this.templateData, {super.key});
@override
State<RowGradeWidget> createState() => _RowGradeWidgetState();
}
class _RowGradeWidgetState extends State<RowGradeWidget> {
Map get _templateData => widget.templateData;
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Padding(
padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w),
child: Row(
children: [
Expanded(
child: Text(
_templateData['name'],
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
Slider(
value: _templateData['value'].toDouble(),
min: 0,
max: 10,
divisions: 10,
activeColor: Colors.blue,
onChanged: (value) {
setState(() {
_templateData['value'] = value.toInt();
});
},
onChangeEnd: (value) {
_templateData['onCallback'](_templateData['value']);
},
),
SizedBox(
width: 15.w,
child: Text(
_templateData['value'].toString(),
textAlign: TextAlign.right,
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white : Colors.grey,
),
),
),
],
),
);
}
}

76
lib/modules/eq_settings/views/widgets/row_input_widget.dart

@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'settings_input_widget.dart';
class RowInputWidget extends StatefulWidget {
final Map templateData;
const RowInputWidget(this.templateData, {super.key});
@override
State<RowInputWidget> createState() => _RowInputWidgetState();
}
class _RowInputWidgetState extends State<RowInputWidget> {
Map get _templateData => widget.templateData;
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return GestureDetector(
onTap: () => _inputBottomSheet(isDarkMode),
behavior: HitTestBehavior.translucent,
child: Padding(
padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w),
child: Row(
children: [
Expanded(
child: Text(
_templateData['name'],
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
Text(
_templateData['value'],
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white : Colors.grey,
),
),
Icon(
Icons.chevron_right,
size: 26.w,
color: Colors.blue,
),
],
),
),
);
}
void _inputBottomSheet(bool isDarkMode) {
Get.bottomSheet(
SettingsInputWidget(_templateData['name'], _templateData['value']),
isScrollControlled: true,
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
).then((value) {
if (value != null) {
setState(() {
_templateData['value'] = value;
});
_templateData['onCallback'](value);
}
});
}
}

242
lib/modules/eq_settings/views/widgets/row_keypress_widget.dart

@ -0,0 +1,242 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
class RowKeypressWidget extends StatefulWidget {
final Map templateData;
const RowKeypressWidget(this.templateData, {super.key});
@override
State<RowKeypressWidget> createState() => _RowKeypressWidgetState();
}
class _RowKeypressWidgetState extends State<RowKeypressWidget> {
Map get _templateData => widget.templateData;
String _getLabel(int value) {
return _templateData['options'].firstWhere((item) => item['value'] == value,
orElse: () => {'label': ''})['label'];
}
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Column(
children: [
Padding(
padding: EdgeInsets.symmetric(vertical: 10.w),
child: Row(
children: [
const Spacer(),
Expanded(
child: Text(
'左',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
Expanded(
child: Text(
'右',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
],
),
),
..._templateData['data']
.map((item) => _rowItem(isDarkMode, item))
.toList(),
],
);
}
Widget _rowItem(bool isDarkMode, Map item) {
return Row(
children: [
Expanded(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 10.w),
child: Text(
item['label'],
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
),
Expanded(
child: GestureDetector(
onTap: () => _radioDialog(isDarkMode, item, 'left'),
behavior: HitTestBehavior.translucent,
child: Padding(
padding: EdgeInsets.symmetric(vertical: 10.w),
child: Text(
_getLabel(item['left']),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white : Colors.grey,
),
),
),
),
),
Expanded(
child: GestureDetector(
onTap: () => _radioDialog(isDarkMode, item, 'right'),
behavior: HitTestBehavior.translucent,
child: Padding(
padding: EdgeInsets.symmetric(vertical: 10.w),
child: Text(
_getLabel(item['right']),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white : Colors.grey,
),
),
),
),
),
],
);
}
void _radioDialog(bool isDarkMode, Map item, String key) {
Get.dialog(
Dialog(
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
child: Padding(
padding: EdgeInsets.all(12.w),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${item['label']}-${key == 'left' ? '左' : '右'}',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
SizedBox(height: 16.h),
Container(
constraints: BoxConstraints(maxHeight: 0.5.sh),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: _templateData['options']
.map<Widget>((option) =>
_radioItem(isDarkMode, option, item, key))
.toList(),
),
),
),
SizedBox(height: 16.h),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr, // 取消
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
),
],
),
],
),
),
),
);
}
Widget _radioItem(bool isDarkMode, Map option, Map item, String key) {
return GestureDetector(
onTap: () {
setState(() {
item[key] = option['value'];
});
Map data = {
'key': item['key'],
key: option['value'],
};
_templateData['onCallback'](data);
Get.back();
},
child: Container(
margin: EdgeInsets.only(bottom: 10.h),
padding: EdgeInsets.all(10.w),
decoration: BoxDecoration(
color: item[key] == option['value']
? (isDarkMode
? Colors.teal[900]!.withValues(alpha: 0.3)
: Colors.teal[50]!)
: (isDarkMode
? Colors.white.withValues(alpha: 0.05)
: Colors.grey[100]!),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: item[key] == option['value']
? (isDarkMode ? Colors.teal[300]! : Colors.teal[600]!)
: Colors.transparent,
width: 1,
),
),
child: Row(
children: [
Expanded(
child: Text(
option['label'],
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black87,
),
overflow: TextOverflow.ellipsis,
),
),
Container(
width: 24.w,
height: 24.w,
decoration: BoxDecoration(
color: item[key] == option['value']
? (isDarkMode ? Colors.teal[300] : Colors.teal[600])
: (isDarkMode
? Colors.white.withValues(alpha: 0.1)
: Colors.grey[300]),
shape: BoxShape.circle,
),
child: item[key] == option['value']
? Icon(
Icons.check,
size: 16.w,
color: Colors.white,
)
: null,
),
],
),
),
);
}
}

187
lib/modules/eq_settings/views/widgets/row_radio_widget.dart

@ -0,0 +1,187 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
class RowRadioWidget extends StatefulWidget {
final Map templateData;
const RowRadioWidget(this.templateData, {super.key});
@override
State<RowRadioWidget> createState() => _RowRadioWidgetState();
}
class _RowRadioWidgetState extends State<RowRadioWidget> {
Map get _templateData => widget.templateData;
String _label = '';
@override
void initState() {
super.initState();
_label = _templateData['options'].firstWhere(
(option) => option['value'] == _templateData['value'],
orElse: () => {'label': ''},
)['label'];
}
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return GestureDetector(
onTap: () => _radioDialog(isDarkMode),
behavior: HitTestBehavior.translucent,
child: Padding(
padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w),
child: Row(
children: [
Expanded(
child: Text(
_templateData['name'],
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
Text(
_label,
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white : Colors.grey,
),
),
Icon(
Icons.chevron_right,
size: 26.w,
color: Colors.blue,
),
],
),
),
);
}
void _radioDialog(bool isDarkMode) {
Get.dialog(
Dialog(
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
child: Padding(
padding: EdgeInsets.all(12.w),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_templateData['name'],
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
SizedBox(height: 16.h),
Container(
constraints: BoxConstraints(maxHeight: 0.5.sh),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: _templateData['options']
.map<Widget>((option) => _item(isDarkMode, option))
.toList(),
),
),
),
SizedBox(height: 16.h),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Get.back(),
child: Text(
'cancel'.tr, // 取消
style: TextStyle(
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
),
],
),
],
),
),
),
);
}
Widget _item(bool isDarkMode, Map option) {
return GestureDetector(
onTap: () {
setState(() {
_label = option['label'];
});
_templateData['value'] = option['value'];
_templateData['onCallback'](option['value']);
Get.back();
},
child: Container(
margin: EdgeInsets.only(bottom: 10.h),
padding: EdgeInsets.all(10.w),
decoration: BoxDecoration(
color: _templateData['value'] == option['value']
? (isDarkMode
? Colors.teal[900]!.withValues(alpha: 0.3)
: Colors.teal[50]!)
: (isDarkMode
? Colors.white.withValues(alpha: 0.05)
: Colors.grey[100]!),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: _templateData['value'] == option['value']
? (isDarkMode ? Colors.teal[300]! : Colors.teal[600]!)
: Colors.transparent,
width: 1,
),
),
child: Row(
children: [
Expanded(
child: Text(
option['label'],
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black87,
),
overflow: TextOverflow.ellipsis,
),
),
Container(
width: 24.w,
height: 24.w,
decoration: BoxDecoration(
color: _templateData['value'] == option['value']
? (isDarkMode ? Colors.teal[300] : Colors.teal[600])
: (isDarkMode
? Colors.white.withValues(alpha: 0.1)
: Colors.grey[300]),
shape: BoxShape.circle,
),
child: _templateData['value'] == option['value']
? Icon(
Icons.check,
size: 16.w,
color: Colors.white,
)
: null,
),
],
),
),
);
}
}

61
lib/modules/eq_settings/views/widgets/row_switch_widget.dart

@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class RowSwitchWidget extends StatefulWidget {
final Map templateData;
const RowSwitchWidget(this.templateData, {super.key});
@override
State<RowSwitchWidget> createState() => _RowSwitchWidgetState();
}
class _RowSwitchWidgetState extends State<RowSwitchWidget> {
Map get _templateData => widget.templateData;
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Padding(
padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w),
child: Row(
children: [
Expanded(
child: Text(
_templateData['name'],
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
if (_templateData['description'] != null)
Text(
_templateData['description'],
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white : Colors.grey,
),
),
SizedBox(
height: 26.w,
child: Transform.scale(
scale: 0.8,
alignment: Alignment.centerRight,
child: Switch(
value: _templateData['value'],
onChanged: (bool value) {
setState(() {
_templateData['value'] = value;
});
_templateData['onCallback'](value);
},
),
),
),
],
),
);
}
}

77
lib/modules/eq_settings/views/widgets/row_timer_widget.dart

@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'select_timer_widget.dart';
class RowTimerWidget extends StatefulWidget {
final Map templateData;
const RowTimerWidget(this.templateData, {super.key});
@override
State<RowTimerWidget> createState() => _RowTimerWidgetState();
}
class _RowTimerWidgetState extends State<RowTimerWidget> {
Map get _templateData => widget.templateData;
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return GestureDetector(
onTap: () => _timerBottomSheet(isDarkMode),
behavior: HitTestBehavior.translucent,
child: Padding(
padding: EdgeInsets.fromLTRB(10.w, 10.w, 0, 10.w),
child: Row(
children: [
Expanded(
child: Text(
_templateData['name'],
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black,
),
),
),
if (_templateData['value'] > 0)
Text(
'${_templateData['value']}分钟',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white : Colors.grey,
),
),
Icon(
Icons.chevron_right,
size: 26.w,
color: Colors.blue,
),
],
),
),
);
}
void _timerBottomSheet(bool isDarkMode) {
Get.bottomSheet(
SelectTimerWidget(_templateData['name'], _templateData['value']),
isScrollControlled: true,
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
).then((value) {
if (value != null) {
setState(() {
_templateData['value'] = value;
});
_templateData['onCallback'](value);
}
});
}
}

305
lib/modules/eq_settings/views/widgets/select_equalizer_widget.dart

@ -0,0 +1,305 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'settings_input_widget.dart';
class SelectEqualizerWidget extends StatefulWidget {
final Map templateData;
const SelectEqualizerWidget(this.templateData, {super.key});
@override
State<SelectEqualizerWidget> createState() => _SelectEqualizerWidgetState();
}
class _SelectEqualizerWidgetState extends State<SelectEqualizerWidget> {
Map get _templateData => widget.templateData;
List _values = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
bool isEdit = false;
final GetStorage _storage = GetStorage();
List _equalizerOptions = [];
@override
void initState() {
super.initState();
if (_templateData['value'].isNotEmpty) {
_values = List.from(_templateData['value']);
}
_equalizerOptions = _storage.read('equalizer_options') ?? [];
}
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: _buildAppBar(isDarkMode),
body: _buildBody(isDarkMode),
);
}
// 构建应用栏
PreferredSizeWidget _buildAppBar(bool isDarkMode) {
return AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
surfaceTintColor: Colors.transparent,
elevation: 0.5,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
onPressed: () => Get.back(),
),
title: Text(
_templateData['name'],
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
centerTitle: true,
actions: [
GestureDetector(
onTap: () {
setState(() {
isEdit = !isEdit;
});
},
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 12.w,
vertical: 6.w,
),
child: Text(
isEdit ? 'cancel'.tr : 'edit'.tr,
style: TextStyle(
color: isDarkMode ? Colors.white : Colors.black87,
fontSize: 16.sp,
),
),
),
),
],
);
}
// 构建主体内容
Widget _buildBody(bool isDarkMode) {
bool isAdd = true;
return ListView(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.w),
children: [
_equalizer(isDarkMode),
Wrap(
spacing: 10.w,
runSpacing: 10.w,
children: [
..._templateData['options'].map((Map option) {
bool isSelected = listEquals(_values, option['value']);
if (isSelected) {
isAdd = false;
}
return _item(isDarkMode, option, isSelected);
}).toList(),
if (isAdd)
GestureDetector(
onTap: () => _inputBottomSheet(isDarkMode),
child: Container(
width: 110.w,
height: 110.w,
alignment: Alignment.center,
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withValues(alpha: 0.05)
: Colors.white,
borderRadius: BorderRadius.circular(8.w),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withValues(alpha: 0.2)
: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Icon(
Icons.add,
size: 40.sp,
color: Colors.green,
),
),
),
],
),
],
);
}
Widget _equalizer(bool isDarkMode) {
return Container(
height: 220.w,
margin: EdgeInsets.only(bottom: 5.w),
child: Row(
children: List.generate(
_templateData['xAxis'].length,
(index) => Expanded(
child: Stack(
children: [
RotatedBox(
quarterTurns: 3,
child: StatefulBuilder(
builder: (BuildContext context, setStateful) {
return Slider(
value: _values[index].toDouble(),
min: -5,
max: 5,
divisions: 10,
activeColor: Colors.green,
onChanged: (value) {
setStateful(() {
_values[index] = value.toInt();
});
},
onChangeEnd: (value) {
setState(() {});
},
);
},
),
),
Align(
alignment: Alignment.bottomCenter,
child: Text(
_templateData['xAxis'][index],
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.grey,
),
),
),
],
),
),
),
),
);
}
Widget _item(bool isDarkMode, Map option, bool isSelected) {
return GestureDetector(
onTap: () {
_templateData['value'] = option['value'];
Get.back(result: true);
},
child: Stack(
children: [
Container(
width: 110.w,
height: 110.w,
alignment: Alignment.center,
decoration: BoxDecoration(
color: isSelected
? Colors.green
: isDarkMode
? Colors.white.withValues(alpha: 0.05)
: Colors.white,
borderRadius: BorderRadius.circular(8.w),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withValues(alpha: 0.2)
: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Text(
option['label'],
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isSelected
? Colors.white
: isDarkMode
? Colors.white
: Colors.black,
),
),
),
if (isEdit && !option['default'])
Positioned(
top: 4.w,
right: 4.w,
child: GestureDetector(
onTap: () {
if (isSelected) {
_templateData['value'] = [];
_templateData['onCallback']([]);
}
_equalizerOptions.removeWhere(
(e) => listEquals(e['value'], option['value']));
_storage.write('equalizer_options', _equalizerOptions);
setState(() {
_templateData['options'].remove(option);
});
},
child: Container(
width: 24.w,
height: 24.w,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(12.w),
),
child: Icon(
Icons.close,
size: 16.sp,
color: Colors.white,
),
),
),
),
],
),
);
}
void _inputBottomSheet(bool isDarkMode) {
Get.bottomSheet(
const SettingsInputWidget('添加均衡器', ''),
isScrollControlled: true,
backgroundColor: isDarkMode ? Colors.grey[800] : Colors.grey[100],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
).then((value) {
if (value != null) {
Map<String, Object> option = {
'label': value,
'value': List.from(_values),
'default': false,
};
_equalizerOptions.add(option);
_storage.write('equalizer_options', _equalizerOptions);
setState(() {
_templateData['options'].add(option);
});
}
});
}
}

231
lib/modules/eq_settings/views/widgets/select_timer_widget.dart

@ -0,0 +1,231 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
class SelectTimerWidget extends StatefulWidget {
final String title;
final int time;
const SelectTimerWidget(this.title, this.time, {super.key});
@override
State<SelectTimerWidget> createState() => _SelectTimerWidgetState();
}
class _SelectTimerWidgetState extends State<SelectTimerWidget> {
late FixedExtentScrollController _hourScrollController;
late FixedExtentScrollController _minuteScrollController;
int _hour = 0;
int _minute = 0;
@override
void initState() {
super.initState();
_hour = widget.time ~/ 60;
_minute = widget.time % 60;
_hourScrollController = FixedExtentScrollController(initialItem: _hour);
_minuteScrollController = FixedExtentScrollController(initialItem: _minute);
}
@override
void dispose() {
super.dispose();
_hourScrollController.dispose();
_minuteScrollController.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.symmetric(vertical: 10.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.title,
style: TextStyle(
fontSize: 16.sp,
color: Colors.black87,
fontWeight: FontWeight.bold,
),
),
GestureDetector(
onTap: () => Get.back(),
child: Container(
width: 26.w,
height: 26.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(26.r),
),
child: Icon(
Icons.close,
size: 18.w,
color: Colors.grey[400],
),
),
)
],
),
),
Divider(
height: 1,
color: Colors.grey[300],
),
SizedBox(
height: 220.w,
child: Row(
children: [
Expanded(
child: CupertinoPicker.builder(
itemExtent: 40.w,
diameterRatio: 1.1,
squeeze: 1.25,
childCount: 24,
selectionOverlay: _selectionOverlayWidget(
isRadiusLeft: true,
),
scrollController: _hourScrollController,
itemBuilder: (BuildContext context, int index) {
return Container(
padding: EdgeInsets.only(right: 10.w),
alignment: Alignment.centerRight,
child: Text(
'$index时',
style: _hour == index
? TextStyle(
color: const Color(0xFFFF5561),
fontSize: 15.sp,
fontWeight: FontWeight.w500,
)
: TextStyle(
color: const Color(0xFF9D9D9D),
fontSize: 15.sp,
),
),
);
},
onSelectedItemChanged: (int index) {
setState(() {
_hour = index;
});
},
),
),
Expanded(
child: CupertinoPicker.builder(
itemExtent: 40.w,
diameterRatio: 1.1,
squeeze: 1.25,
childCount: 60,
selectionOverlay: _selectionOverlayWidget(
isRadiusRight: true,
),
scrollController: _minuteScrollController,
itemBuilder: (BuildContext context, int index) {
return Container(
padding: EdgeInsets.only(left: 10.w),
alignment: Alignment.centerLeft,
child: Text(
'$index分',
style: _minute == index
? TextStyle(
color: const Color(0xFFFF5561),
fontSize: 15.sp,
fontWeight: FontWeight.w500,
)
: TextStyle(
color: const Color(0xFF9D9D9D),
fontSize: 15.sp,
),
),
);
},
onSelectedItemChanged: (int index) {
setState(() {
_minute = index;
});
},
),
),
],
),
),
Padding(
padding: EdgeInsets.only(bottom: 20.w),
child: Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => Get.back(),
child: Container(
height: 48.w,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(12.r),
),
child: Text(
'cancel'.tr, // 取消
style: TextStyle(
fontSize: 16.sp,
color: Colors.black,
),
),
),
),
),
10.horizontalSpace,
Expanded(
child: GestureDetector(
onTap: () {
int time = _hour * 60 + _minute;
Get.back(result: time);
},
child: Container(
height: 48.w,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(12.r),
),
child: Text(
'confirm'.tr, // 确认
style: TextStyle(
fontSize: 16.sp,
color: Colors.white,
),
),
),
),
),
],
),
),
],
),
);
}
Widget _selectionOverlayWidget({
bool isRadiusLeft = false,
bool isRadiusRight = false,
}) {
return Container(
decoration: BoxDecoration(
color: const Color.fromARGB(45, 255, 85, 97),
borderRadius: BorderRadius.horizontal(
left: isRadiusLeft ? Radius.circular(8.w) : Radius.zero,
right: isRadiusRight ? Radius.circular(8.w) : Radius.zero,
),
),
);
}
}

162
lib/modules/eq_settings/views/widgets/settings_input_widget.dart

@ -0,0 +1,162 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
class SettingsInputWidget extends StatefulWidget {
final String title;
final String inputText;
const SettingsInputWidget(this.title, this.inputText, {super.key});
@override
State<SettingsInputWidget> createState() => _SettingsInputWidgetState();
}
class _SettingsInputWidgetState extends State<SettingsInputWidget> {
final TextEditingController _inputController = TextEditingController();
@override
void initState() {
super.initState();
_inputController.text = widget.inputText;
}
@override
void dispose() {
_inputController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.symmetric(vertical: 10.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
widget.title,
style: TextStyle(
fontSize: 16.sp,
color: Colors.black87,
fontWeight: FontWeight.bold,
),
),
GestureDetector(
onTap: () => Get.back(),
child: Container(
width: 26.w,
height: 26.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(26.r),
),
child: Icon(
Icons.close,
size: 18.w,
color: Colors.grey[400],
),
),
)
],
),
),
Divider(
height: 1,
color: Colors.grey[300],
),
Container(
margin: EdgeInsets.symmetric(vertical: 20.w),
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: TextField(
controller: _inputController,
style: TextStyle(
color: Colors.black87,
fontSize: 16.sp,
),
decoration: InputDecoration(
hintText: 'maxCharacters'.tr,
hintStyle: TextStyle(
color: Colors.black54,
fontSize: 16.sp,
),
isDense: true,
fillColor: Colors.transparent,
border: InputBorder.none,
contentPadding: EdgeInsets.all(10.w),
),
),
),
Padding(
padding: EdgeInsets.only(bottom: 20.w),
child: Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => Get.back(),
child: Container(
height: 48.w,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(12.r),
),
child: Text(
'cancel'.tr, // 取消
style: TextStyle(
fontSize: 16.sp,
color: Colors.black,
),
),
),
),
),
10.horizontalSpace,
Expanded(
child: GestureDetector(
onTap: () {
if (_inputController.text.isNotEmpty) {
Get.back(result: _inputController.text);
}
},
child: Container(
height: 48.w,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(12.r),
),
child: Text(
'confirm'.tr, // 确认
style: TextStyle(
fontSize: 16.sp,
color: Colors.white,
),
),
),
),
),
],
),
),
],
),
);
}
}

6
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();
}); });
} }
@ -542,9 +542,7 @@ class HomeController extends GetxController {
} }
// 使用默认耳机图片 // 使用默认耳机图片
return isDarkMode return 'assets/images/defaultPicture.png';
? 'assets/images/headphone_new3.png'
: 'assets/images/headphone_new4.png';
} }
/// 判断是否为网络图片 /// 判断是否为网络图片

56
lib/modules/home/views/home_view.dart

@ -85,10 +85,12 @@ class HomeView extends GetView<HomeController> {
// 顶部标题栏 // 顶部标题栏
_buildAppBar(isDarkMode), _buildAppBar(isDarkMode),
SizedBox(height: 10.h),
// 顶部图片 // 顶部图片
_buildEarbudsImage(), _buildEarbudsImage(),
SizedBox(height: 20.h), SizedBox(height: 10.h),
// 设备状态卡片 // 设备状态卡片
IntroStepBuilder( IntroStepBuilder(
@ -210,26 +212,10 @@ class HomeView extends GetView<HomeController> {
height: 220.h, height: 220.h,
child: PageView( child: PageView(
children: [ children: [
// Center(
// child: ClipRRect(
// // 添加 ClipRRect 来实现圆角
// borderRadius: BorderRadius.circular(16.r), // 使用与其他部件一致的圆角大小
// child: SizedBox(
// width: double.infinity,
// height: 200.h,
// child: Image.asset(
// isDarkMode
// ? 'assets/images/robot_light.png'
// : 'assets/images/robot_light.png',
// fit: BoxFit.fitWidth,
// ),
// ),
// ),
// ),
Center( Center(
child: ClipRRect( child: ClipRRect(
// 添加 ClipRRect // 添加 ClipRRect
borderRadius: BorderRadius.circular(16.r), borderRadius: BorderRadius.circular(20.r),
child: GetBuilder<HomeController>( child: GetBuilder<HomeController>(
builder: (controller) { builder: (controller) {
final imagePath = controller.getDeviceImagePath(isDarkMode); final imagePath = controller.getDeviceImagePath(isDarkMode);
@ -238,16 +224,16 @@ class HomeView extends GetView<HomeController> {
return isNetwork return isNetwork
? Image.network( ? Image.network(
imagePath, imagePath,
width: double.infinity,
height: 220.h, height: 220.h,
fit: BoxFit.contain, fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) { errorBuilder: (context, error, stackTrace) {
// 网络图片加载失败时使用默认图片 // 网络图片加载失败时使用默认图片
return Image.asset( return Image.asset(
isDarkMode 'assets/images/defaultPicture.png',
? 'assets/images/headphone_new3.png' width: double.infinity,
: 'assets/images/headphone_new4.png',
height: 220.h, height: 220.h,
fit: BoxFit.contain, fit: BoxFit.cover,
); );
}, },
loadingBuilder: (context, child, loadingProgress) { loadingBuilder: (context, child, loadingProgress) {
@ -268,16 +254,16 @@ class HomeView extends GetView<HomeController> {
) )
: Image.asset( : Image.asset(
imagePath, imagePath,
width: double.infinity,
height: 220.h, height: 220.h,
fit: BoxFit.contain, fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) { errorBuilder: (context, error, stackTrace) {
// 本地图片加载失败时使用默认图片 // 本地图片加载失败时使用默认图片
return Image.asset( return Image.asset(
isDarkMode 'assets/images/defaultPicture.png',
? 'assets/images/headphone_new3.png' width: double.infinity,
: 'assets/images/headphone_new4.png',
height: 220.h, height: 220.h,
fit: BoxFit.contain, fit: BoxFit.cover,
); );
}, },
); );
@ -285,22 +271,6 @@ class HomeView extends GetView<HomeController> {
), ),
), ),
), ),
// Center(
// child: ClipRRect(
// // 添加 ClipRRect
// borderRadius: BorderRadius.circular(16.r),
// child: SizedBox(
// width: double.infinity,
// height: 200.h,
// child: Image.asset(
// isDarkMode
// ? 'assets/images/glasses_light.png'
// : 'assets/images/glasses_light.png',
// fit: BoxFit.fitWidth,
// ),
// ),
// ),
// ),
], ],
), ),
); );

9
lib/modules/meeting/controllers/meeting_details_controller.dart

@ -15,6 +15,7 @@ import '../../../core/utils/synchrodata.dart';
import '../../../data/services/db/sqflite_api.dart'; import '../../../data/services/db/sqflite_api.dart';
import '../../../data/services/meeting/meeting_task_service.dart'; import '../../../data/services/meeting/meeting_task_service.dart';
import '../../../data/services/meeting/meeting_upload_service.dart'; import '../../../data/services/meeting/meeting_upload_service.dart';
import '../../../data/services/meeting/summary_task_service.dart';
import '../model/meeting_details_model.dart'; import '../model/meeting_details_model.dart';
import '../model/meeting_model.dart'; import '../model/meeting_model.dart';
@ -30,6 +31,7 @@ class MeetingDetailsController extends GetxController
final _meetingUploadService = Get.find<MeetingUploadService>(); final _meetingUploadService = Get.find<MeetingUploadService>();
final _meetingTaskService = Get.find<MeetingTaskService>(); final _meetingTaskService = Get.find<MeetingTaskService>();
final _summaryTaskService = Get.find<SummaryTaskService>();
final PlayerController playerController = PlayerController(); final PlayerController playerController = PlayerController();
@ -97,8 +99,8 @@ class MeetingDetailsController extends GetxController
if (value == 2) { if (value == 2) {
textList.value = _meetingTaskService.textList; textList.value = _meetingTaskService.textList;
} else if (value == 3) { } else if (value == 3) {
meetingDetails.value.summary = _meetingTaskService.summary; meetingDetails.value.summary = _summaryTaskService.summary;
meetingDetails.value.overview = _meetingTaskService.overview; meetingDetails.value.overview = _summaryTaskService.overview;
summaryNumberModifications.value++; summaryNumberModifications.value++;
} }
} }
@ -127,6 +129,7 @@ class MeetingDetailsController extends GetxController
final data = await SqfliteApi.getMeetingDetails(id); final data = await SqfliteApi.getMeetingDetails(id);
meetingData.value = MeetingModel.fromJson(data); meetingData.value = MeetingModel.fromJson(data);
meetingDetails.value = MeetingDetailsModel.fromJson(data['details']); meetingDetails.value = MeetingDetailsModel.fromJson(data['details']);
_everType = meetingDetails.value.tasktype + 1;
_meetingTaskService.tasktype.value = meetingDetails.value.tasktype; _meetingTaskService.tasktype.value = meetingDetails.value.tasktype;
if (meetingDetails.value.tasktype >= 3) { if (meetingDetails.value.tasktype >= 3) {
dragOffset.value = 0; dragOffset.value = 0;
@ -274,6 +277,6 @@ class MeetingDetailsController extends GetxController
_everType = 3; _everType = 3;
await SqfliteApi.editMeeting(id, {'tasktype': 2}); await SqfliteApi.editMeeting(id, {'tasktype': 2});
_meetingTaskService.tasktype.value = 2; _meetingTaskService.tasktype.value = 2;
_meetingTaskService.aiServiceMessage(id, text, prompt, outline); _summaryTaskService.addTaskList(id, text, prompt, outline);
} }
} }

47
lib/modules/meeting/controllers/meeting_home_controller.dart

@ -20,6 +20,7 @@ import 'package:share_plus/share_plus.dart';
import '../../../data/services/meeting/meeting_task_service.dart'; import '../../../data/services/meeting/meeting_task_service.dart';
import '../../../data/services/meeting/meeting_upload_service.dart'; import '../../../data/services/meeting/meeting_upload_service.dart';
import '../../../data/services/meeting/summary_task_service.dart';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
import 'meeting_controller.dart'; import 'meeting_controller.dart';
@ -28,6 +29,7 @@ class MeetingHomeController extends GetxController {
final meetingController = Get.find<MeetingController>(); final meetingController = Get.find<MeetingController>();
final meetingUploadService = Get.find<MeetingUploadService>(); final meetingUploadService = Get.find<MeetingUploadService>();
final meetingTaskService = Get.find<MeetingTaskService>(); final meetingTaskService = Get.find<MeetingTaskService>();
final summaryTaskService = Get.find<SummaryTaskService>();
final AudioPlayer _audioPlayer = AudioPlayer(); final AudioPlayer _audioPlayer = AudioPlayer();
@ -135,6 +137,7 @@ class MeetingHomeController extends GetxController {
.toList(); .toList();
} }
//未使用
void loadTranslatAudio() async { void loadTranslatAudio() async {
try { try {
final appDir = await appDocDir; // 先 await 获取目录 final appDir = await appDocDir; // 先 await 获取目录
@ -289,9 +292,9 @@ class MeetingHomeController extends GetxController {
} }
} }
// 新增:将翻译音频归档到 LocalAudio 并返回新路径(优先 rename,失败再 copy+delete) // 新增(2025-11-04):将翻译音频归档到 LocalAudio 并返回新路径(优先 rename,失败再 copy+delete)
Future<String> _archiveTranslatToLocal(String srcPath) async { Future<String> _archiveTranslatToLocal(String srcPath) async {
// 中文注释:获取应用文档目录并确保 LocalAudio 目录存在 // 获取应用文档目录并确保 LocalAudio 目录存在
final appDir = await appDocDir; final appDir = await appDocDir;
final destDir = Directory(path.join(appDir.path, 'LocalAudio')); final destDir = Directory(path.join(appDir.path, 'LocalAudio'));
if (!await destDir.exists()) { if (!await destDir.exists()) {
@ -327,8 +330,9 @@ class MeetingHomeController extends GetxController {
for (final originalPath in selected) { for (final originalPath in selected) {
try { try {
// 中文注释:根据开关决定是否将翻译音频迁移到 LocalAudio // 中文注释:根据开关决定是否将翻译音频迁移到 LocalAudio
final usePath = final usePath = archiveToLocal
archiveToLocal ? await _archiveTranslatToLocal(originalPath) : originalPath; ? await _archiveTranslatToLocal(originalPath)
: originalPath;
// 中文注释:计算音频时长与创建时间 // 中文注释:计算音频时长与创建时间
await _audioPlayer.setAudioSource(AudioSource.file(usePath)); await _audioPlayer.setAudioSource(AudioSource.file(usePath));
@ -370,7 +374,8 @@ class MeetingHomeController extends GetxController {
Logger.error('importFailed' Logger.error('importFailed'
.tr .tr
.replaceAll('{filePath}', originalPath) .replaceAll('{filePath}', originalPath)
.replaceAll('{error}', e.toString())); // 对应中文:导入文件失败: {filePath}, error: {error} .replaceAll('{error}',
e.toString())); // 对应中文:导入文件失败: {filePath}, error: {error}
} }
} }
EasyLoading.dismiss(); EasyLoading.dismiss();
@ -381,12 +386,30 @@ class MeetingHomeController extends GetxController {
String fileName = ""; String fileName = "";
bool isParamProvided = filePath != null; // 更清晰的变量名 bool isParamProvided = filePath != null; // 更清晰的变量名
String audioType = 'IMPORT'; // 默认类型为IMPORT String audioType = 'IMPORT'; // 默认类型为IMPORT
String effectivePath = filePath ?? '';
if (filePath != null) { if (filePath != null) {
// 正确截取文件名(跨平台安全方式) // 正确截取文件名(跨平台安全方式)
fileName = path.basename(filePath); fileName = path.basename(filePath);
// 检查是否来自翻译模块 // 检查是否来自翻译模块(兼容不同平台路径分隔符)
if (filePath.contains('/TranslatAudio/')) { final isFromTranslat = filePath.contains('/TranslatAudio/') ||
audioType = 'TRANSLAT'; // 设置为翻译音频类型 filePath.contains('\\TranslatAudio\\');
if (isFromTranslat) {
try {
// 实现——导入前将翻译音频归档到LocalAudio
effectivePath = await _archiveTranslatToLocal(filePath);
audioType = 'TRANSLAT'; // 归档后统一标记为TRANSLAT类型
fileName = path.basename(effectivePath);
} catch (e) {
// 归档失败则回退为原路径、保持TRANSLAT类型以避免中断导入
audioType = 'TRANSLAT';
effectivePath = filePath;
Logger.error('importFailed'
.tr
.replaceAll('{filePath}', filePath)
.replaceAll('{error}', e.toString())); // 翻译键代表的中文文本内容
}
} else {
effectivePath = filePath;
} }
} else { } else {
FilePickerResult? result = await FilePicker.platform.pickFiles( FilePickerResult? result = await FilePicker.platform.pickFiles(
@ -410,7 +433,7 @@ class MeetingHomeController extends GetxController {
} }
// 使用文件路径而不是字节数据 // 使用文件路径而不是字节数据
filePath = file.path ?? await _copyFileToLocal(file); effectivePath = file.path ?? await _copyFileToLocal(file);
fileName = file.name; fileName = file.name;
} else { } else {
EasyLoading.dismiss(); EasyLoading.dismiss();
@ -419,7 +442,7 @@ class MeetingHomeController extends GetxController {
} }
final meetingId = final meetingId =
await insertMeetingAudio(fileName, filePath, type: audioType); await insertMeetingAudio(fileName, effectivePath, type: audioType);
if (isParamProvided) { if (isParamProvided) {
Get.toNamed(Routes.meetingDetails, arguments: { Get.toNamed(Routes.meetingDetails, arguments: {
@ -449,10 +472,12 @@ class MeetingHomeController extends GetxController {
}; };
final meetingId = await SqfliteApi.insertMeeting(data); final meetingId = await SqfliteApi.insertMeeting(data);
if (meetingId != null) { if (meetingId != null) {
// 中文注释:根据类型选择上传分类;LOCAL 走 LocalAudio,其它走 ExternalAudio
final uploadBucket = type == 'LOCAL' ? 'LocalAudio' : 'ExternalAudio';
meetingUploadService.addUpload( meetingUploadService.addUpload(
meetingId, meetingId,
filePath, filePath,
'ExternalAudio', uploadBucket,
isUpload: isUpload, isUpload: isUpload,
); );
originalDataList.insert(0, { originalDataList.insert(0, {

38
lib/modules/meeting/controllers/mind_map_controller.dart

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:image_gallery_saver_plus/image_gallery_saver_plus.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter/webview_flutter.dart';
@ -19,6 +20,8 @@ class MindMapController extends GetxController {
final double _width = 1.sw - 25.w; final double _width = 1.sw - 25.w;
bool isWebViewReady = false; bool isWebViewReady = false;
int imageType = 1;
late final Worker _summaryNumberListener; late final Worker _summaryNumberListener;
@override @override
@ -170,18 +173,29 @@ class MindMapController extends GetxController {
final String dataUrl = message.message; final String dataUrl = message.message;
final String base64Str = dataUrl.split(',').last; final String base64Str = dataUrl.split(',').last;
final Uint8List bytes = base64.decode(base64Str); final Uint8List bytes = base64.decode(base64Str);
final String fileName = if (imageType == 1) {
'${_meetingDetailsController.meetingData.value.title}_mindmap.png'; // 保存图片
final tempDir = await getTemporaryDirectory(); final result = await ImageGallerySaverPlus.saveImage(
final File file = File('${tempDir.path}/$fileName'); bytes,
await file.writeAsBytes(bytes); quality: 100,
final result = await SharePlus.instance.share(ShareParams( );
text: 'shareMindMap'.tr, //对应中文:分享思维导图 if (result['isSuccess']) {
EasyLoading.showToast('保存成功');
files: [XFile(file.path)], }
)); } else {
if (result.status == ShareResultStatus.success) { // 分享图片
EasyLoading.showToast('shareSuccess'.tr); //对应中文:分享成功 final String fileName =
'${_meetingDetailsController.meetingData.value.title}_mindmap.png';
final tempDir = await getTemporaryDirectory();
final File file = File('${tempDir.path}/$fileName');
await file.writeAsBytes(bytes);
final result = await SharePlus.instance.share(ShareParams(
text: 'shareMindMap'.tr, //对应中文:分享思维导图
files: [XFile(file.path)],
));
if (result.status == ShareResultStatus.success) {
EasyLoading.showToast('shareSuccess'.tr); //对应中文:分享成功
}
} }
} }
} }

28
lib/modules/meeting/views/bottomSheet/share_bottom_sheet.dart

@ -209,6 +209,33 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
); );
} }
: null), : null),
_shareItem(
Icons.save_alt,
'保存思维导图',
onTap: _controller.meetingDetails.value.tasktype >= 3
? () {
Get.back();
final mindMapController =
Get.find<MindMapController>();
mindMapController.imageType = 1;
if (mindMapController.isWebViewReady) {
mindMapController.webViewController
.runJavaScript('captureImage()');
} else {
mindMapController.webViewController
.setNavigationDelegate(
NavigationDelegate(
onPageFinished: (String url) {
mindMapController.isWebViewReady = true;
mindMapController.webViewController
.runJavaScript('captureImage()');
},
),
);
}
}
: null,
),
_shareItem( _shareItem(
Icons.lan, Icons.lan,
'shareMindMap'.tr, // 对应中文:分享思维导图 'shareMindMap'.tr, // 对应中文:分享思维导图
@ -218,6 +245,7 @@ class _ShareBottomSheetState extends State<ShareBottomSheet> {
Get.back(); Get.back();
final mindMapController = final mindMapController =
Get.find<MindMapController>(); Get.find<MindMapController>();
mindMapController.imageType = 2;
if (mindMapController.isWebViewReady) { if (mindMapController.isWebViewReady) {
mindMapController.webViewController mindMapController.webViewController
.runJavaScript('captureImage()'); .runJavaScript('captureImage()');

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;
} }
} }

87
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(
@ -172,7 +173,7 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
// 设备图标 // 设备图标
Widget _buildDeviceIcon(bool isDarkMode) { Widget _buildDeviceIcon(bool isDarkMode) {
return Container( return Container(
padding: EdgeInsets.all(16.w), padding: EdgeInsets.all(6.w),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDarkMode color: isDarkMode
? Colors.white.withValues(alpha: 0.05) ? Colors.white.withValues(alpha: 0.05)
@ -190,46 +191,52 @@ class OtaUpgradeView extends GetView<OtaUpgradeController> {
final imagePath = controller.lastConnectedDeviceImage; final imagePath = controller.lastConnectedDeviceImage;
if (imagePath == null || imagePath.isEmpty) { if (imagePath == null || imagePath.isEmpty) {
return Image.asset( return ClipRRect(
isDarkMode borderRadius: BorderRadius.circular(12.r),
? 'assets/images/headphone_new3.png' child: SizedBox(
: 'assets/images/headphone_new4.png', width: 120.h,
height: 100.h, height: 120.h,
fit: BoxFit.contain, child: Image.asset(
'assets/images/defaultPicture.jpg',
fit: BoxFit.cover,
),
),
); );
} }
// 网络图片 // 网络图片(正方形裁剪)
return Image.network( return ClipRRect(
imagePath, borderRadius: BorderRadius.circular(16.r),
height: 100.h, child: SizedBox(
fit: BoxFit.contain, width: 120.h,
loadingBuilder: (context, child, loadingProgress) { height: 120.h,
if (loadingProgress == null) return child; child: Image.network(
return SizedBox( imagePath,
height: 100.h,
child: Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
strokeWidth: 2,
color: isDarkMode ? Colors.white : Colors.blue,
),
),
);
},
errorBuilder: (context, error, stackTrace) {
// 网络图片加载失败时使用默认图片
return Image.asset(
isDarkMode
? 'assets/images/headphone_new3.png'
: 'assets/images/headphone_new4.png',
height: 100.h,
fit: BoxFit.contain, fit: BoxFit.contain,
); loadingBuilder: (context, child, loadingProgress) {
}, if (loadingProgress == null) return child;
return SizedBox(
height: 100.h,
child: Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
strokeWidth: 2,
color: isDarkMode ? Colors.white : Colors.blue,
),
),
);
},
errorBuilder: (context, error, stackTrace) {
return Image.asset(
'assets/images/defaultPicture.jpg',
fit: BoxFit.cover,
);
},
),
),
); );
}, },
), ),
@ -827,7 +834,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);
} }

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

@ -2,7 +2,7 @@ import 'dart:io';
import 'dart:async'; import 'dart:async';
import 'package:app_settings/app_settings.dart'; import 'package:app_settings/app_settings.dart';
import 'package:voitrans/data/models/appconfig_model.dart'; import '/data/models/appconfig_model.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@ -512,7 +512,7 @@ class PairingController extends GetxController with WidgetsBindingObserver {
/// 开始初始检查 /// 开始初始检查
void _startInitialCheck() { void _startInitialCheck() {
_transition(PairingState.checking); // _transition(PairingState.checking);
_checkCurrentStatus(); _checkCurrentStatus();
} }

6
lib/modules/qqmusic/views/qqmusic_playlist_view.dart

@ -1,9 +1,9 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../agent/widgets/circular_progress_button.dart'; import '/modules/agent/widgets/circular_progress_button.dart';
import '../controllers/qqmusic_playlist_controller.dart'; import '/modules/qqmusic/controllers/qqmusic_playlist_controller.dart';
import '/../../data/models/song_model.dart'; // Use the data model instead import '/data/models/song_model.dart'; // Use the data model instead
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
class QqmusicPlaylistView extends GetView<QQMusicPlaylistController> { class QqmusicPlaylistView extends GetView<QQMusicPlaylistController> {

11
lib/modules/splash/controllers/splash_controller.dart

@ -10,6 +10,9 @@ import 'package:http/http.dart' as http;
import '../../../data/models/appconfig.dart'; import '../../../data/models/appconfig.dart';
import '../../../data/models/appconfig_model.dart'; import '../../../data/models/appconfig_model.dart';
import '../../../data/models/user_Info.dart'; import '../../../data/models/user_Info.dart';
import '../../../data/services/meeting/meeting_task_service.dart';
import '../../../data/services/meeting/meeting_upload_service.dart';
import '../../../data/services/meeting/summary_task_service.dart';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
import '../../../data/services/network/api.dart'; import '../../../data/services/network/api.dart';
import '../../../data/services/version_update_service.dart'; import '../../../data/services/version_update_service.dart';
@ -266,8 +269,16 @@ class SplashController extends GetxController {
final isAppConfigInitialized = AppConfig.isInitialized(); final isAppConfigInitialized = AppConfig.isInitialized();
// 检查登录状态和配置初始化状态 // 检查登录状态和配置初始化状态
if (token != null && token.isNotEmpty && isAppConfigInitialized) { if (token != null && token.isNotEmpty && isAppConfigInitialized) {
Get.find<MeetingUploadService>();
Get.find<MeetingTaskService>();
Get.find<SummaryTaskService>();
Get.offAllNamed(Routes.pairing); Get.offAllNamed(Routes.pairing);
} else { } else {
_storage.remove('meeting_task_list');
_storage.remove('meeting_task_finish_list');
_storage.remove('meeting_upload_list');
_storage.remove('operation_records');
_storage.remove('operation_delids');
Get.offAllNamed(Routes.login); Get.offAllNamed(Routes.login);
} }
} }

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

@ -2,12 +2,15 @@ import 'dart:io';
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:native_plugin/native_plugin.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:pip/pip.dart';
import '../../../data/services/ast_service.dart'; import '../../../data/services/ast_service.dart';
import '../../../data/services/bluetooth_manager.dart'; import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/music_manager.dart'; import '../../../data/services/music_manager.dart';
@ -48,8 +51,13 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
final BluetoothManager bluetoothManager = Get.find<BluetoothManager>(); final BluetoothManager bluetoothManager = Get.find<BluetoothManager>();
final BleManager bleManager = Get.find<BleManager>(); final BleManager bleManager = Get.find<BleManager>();
final _musiceManager = Get.find<MusicManager>(); final _musiceManager = Get.find<MusicManager>();
late final UsageStatsService _usageService; //用量统计服务 late final TranslationUsageService _usageService;
// 安卓悬浮窗管理器
late final FloatingWindowManager _floatingWindowManager; late final FloatingWindowManager _floatingWindowManager;
// 苹果悬浮窗管理器
late final Pip _pip;
late final NativePlugin _nativePlugin;
int _pipContentView = 0;
// ==================== 悬浮窗相关 ==================== // ==================== 悬浮窗相关 ====================
final isFloatingWindowEnabled = false.obs; // 悬浮窗状态 final isFloatingWindowEnabled = false.obs; // 悬浮窗状态
@ -79,6 +87,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
RxInt activeSpeaker = 0.obs; // 0=无激活, 1=按钮1, 2=按钮2 RxInt activeSpeaker = 0.obs; // 0=无激活, 1=按钮1, 2=按钮2
RxInt lastActiveSpeaker = 0.obs; // 上一次状态 RxInt lastActiveSpeaker = 0.obs; // 上一次状态
final isRecognizing = false.obs; final isRecognizing = false.obs;
final isPreparing = false.obs;
final isTranslating = false.obs; final isTranslating = false.obs;
final isTtsEnabled = true.obs; final isTtsEnabled = true.obs;
final isRecording = false.obs; final isRecording = false.obs;
@ -135,7 +144,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
// 底部控制栏高度 // 底部控制栏高度
double _bottomBarHeight = 0.0; double _bottomBarHeight = 0.0;
/// 初始化悬浮窗管理器 /// 初始化安卓悬浮窗管理器
void _initFloatingWindowManager() { void _initFloatingWindowManager() {
try { try {
if (!Get.isRegistered<FloatingWindowManager>()) { if (!Get.isRegistered<FloatingWindowManager>()) {
@ -148,15 +157,70 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
} }
} }
/// 初始化苹果悬浮窗管理器
Future<void> initPlatformState() async {
_pip = Pip();
_nativePlugin = NativePlugin();
try {
isFloatingWindowEnabled.value = await _pip.isActived();
await _pip.registerStateChangedObserver(
PipStateChangedObserver(
onPipStateChanged: (state, error) {
isFloatingWindowEnabled.value = state == PipState.pipStateStarted;
if (state == PipState.pipStateFailed) {
_pip.dispose();
}
},
),
);
} on PlatformException {
isFloatingWindowEnabled.value = false;
}
}
/// 切换悬浮窗功能 /// 切换悬浮窗功能
Future<void> toggleFloatingWindow() async { Future<void> toggleFloatingWindow() async {
if (isFloatingWindowEnabled.value) { if (isFloatingWindowEnabled.value) {
await _disableFloatingWindow(); if (Platform.isAndroid) {
await _disableFloatingWindow();
} else if (Platform.isIOS) {
await _pip.dispose();
}
} else { } else {
await _enableFloatingWindow(); if (Platform.isAndroid) {
await _enableFloatingWindow();
} else if (Platform.isIOS) {
await _startPip();
}
} }
} }
Future<void> _startPip() async {
if (_pipContentView == 0) {
_pipContentView = await _nativePlugin.createPipContentView();
}
final options = PipOptions(
autoEnterEnabled: true,
aspectRatioX: 375,
aspectRatioY: 400,
sourceRectHintLeft: 0,
sourceRectHintTop: 0,
sourceRectHintRight: 0,
sourceRectHintBottom: 0,
seamlessResizeEnabled: true,
useExternalStateMonitor: true,
externalStateMonitorInterval: 100,
contentView: _pipContentView,
preferredContentWidth: 375,
preferredContentHeight: 400,
controlStyle: 2,
);
await _pip.setup(options);
await _pip.start();
}
/// 启用悬浮窗功能 /// 启用悬浮窗功能
Future<void> _enableFloatingWindow() async { Future<void> _enableFloatingWindow() async {
// 检查是否在支持悬浮窗的模式 // 检查是否在支持悬浮窗的模式
@ -217,16 +281,24 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
_languageManager.getChineseNameByAsrCode(item.targetLanguageCode) ?? _languageManager.getChineseNameByAsrCode(item.targetLanguageCode) ??
'unknown'; 'unknown';
final data = FloatingWindowData( if (Platform.isAndroid) {
sourceText: item.sourceText, final data = FloatingWindowData(
translatedText: item.translatedText, sourceText: item.sourceText,
sourceLanguage: sourceLanguageName, translatedText: item.translatedText,
targetLanguage: targetLanguageName, sourceLanguage: sourceLanguageName,
timestamp: item.timestamp, targetLanguage: targetLanguageName,
isIntermediate: item.isIntermediate, timestamp: item.timestamp,
); isIntermediate: item.isIntermediate,
);
await _floatingWindowManager.updateFloatingWindowContent(data); await _floatingWindowManager.updateFloatingWindowContent(data);
} else if (Platform.isIOS) {
await _nativePlugin.updatePipContentView({
'viewId': _pipContentView,
'cn': item.sourceText,
'en': item.translatedText,
'lang': '$sourceLanguageName -> $targetLanguageName',
});
}
} }
/// 更改翻译模式 /// 更改翻译模式
@ -234,6 +306,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();
// 检查悬浮窗支持情况 // 检查悬浮窗支持情况
@ -376,8 +450,13 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
_timerManager = RecordTimerController(); _timerManager = RecordTimerController();
_historyManager = TranslationHistoryManager(); _historyManager = TranslationHistoryManager();
// 初始化悬浮窗管理器 if (Platform.isAndroid) {
_initFloatingWindowManager(); // 初始化安卓悬浮窗管理器
_initFloatingWindowManager();
} else if (Platform.isIOS) {
// 初始化苹果悬浮窗管理器
initPlatformState();
}
// 预先检查权限,避免在用户交互时阻塞 // 预先检查权限,避免在用户交互时阻塞
_preCheckPermissions(); _preCheckPermissions();
@ -401,7 +480,11 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
targetLanguageCode.value = targetAsrCode; targetLanguageCode.value = targetAsrCode;
} }
// 加载历史记录 // 初始化翻译模式(用于历史隔离的初始加载)
final initMode = Get.arguments?['translationId'] ?? 'simultaneous';
_historyManager.setMode(initMode);
// 按初始模式加载历史
loadTranslationHistory(); loadTranslationHistory();
// 初始化服务 // 初始化服务
@ -410,9 +493,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);
@ -592,10 +674,10 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
await _translationService.initialize(); await _translationService.initialize();
// 初始化 ASR 服务时,明确指定需要支持的语言 // 初始化 ASR 服务时,明确指定需要支持的语言
final List<String> asrSupportedLanguages = [ // final List<String> asrSupportedLanguages = [
sourceLanguageCode.value, // sourceLanguageCode.value,
targetLanguageCode.value // targetLanguageCode.value
]; // ];
//Logger.info('2初始化ASR服务,支持语言: $asrSupportedLanguages'); //Logger.info('2初始化ASR服务,支持语言: $asrSupportedLanguages');
//await _asrService.initialize(supportedLanguages: asrSupportedLanguages); //await _asrService.initialize(supportedLanguages: asrSupportedLanguages);
@ -664,6 +746,15 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
// _asrService.dispose(); // _asrService.dispose();
// _astService.dispose(); // _astService.dispose();
if (Platform.isAndroid) {
_disableFloatingWindow();
} else if (Platform.isIOS) {
_pip.dispose();
if (_pipContentView != 0) {
_nativePlugin.disposePipContentView(_pipContentView);
}
}
// 释放管理器资源 // 释放管理器资源
_timerManager.dispose(); _timerManager.dispose();
_historyManager.dispose(); _historyManager.dispose();
@ -758,10 +849,16 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
await _initializeSession(); await _initializeSession();
await _configureAudioMode(); await _configureAudioMode();
if (currentMode.value != 'faceToFace') { if (currentMode.value != 'faceToFace') {
if ((currentMode.value == 'audioVideo' ||
currentMode.value == 'call') &&
!bleManager.isCodecActive) {
return;
}
await _startAsrService(); await _startAsrService();
await _finalizeRecognitionStart(); await _finalizeRecognitionStart();
} }
} catch (e) { } catch (e) {
isPreparing.value = false;
isRecognizing.value = false; isRecognizing.value = false;
Logger.error('启动语音识别失败: ${e.toString()}'); Logger.error('启动语音识别失败: ${e.toString()}');
} }
@ -807,7 +904,18 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
/// 配置音视频模式 /// 配置音视频模式
Future<void> _configureAudioVideoMode() async { Future<void> _configureAudioVideoMode() async {
bleManager.openDecoder(); isPreparing.value = true;
await bleManager.openDecoder();
int attempts = 0;
while (!bleManager.isCodecActive && attempts < 10) {
await Future.delayed(const Duration(milliseconds: 150));
attempts++;
}
isPreparing.value = false;
if (!bleManager.isCodecActive) {
Get.snackbar('tip'.tr, 'operationFailedPleaseRetry'.tr);
throw Exception('lxm--BLE编解码打开失败');
}
_audioSourceType = true; _audioSourceType = true;
isTtsEnabled.value = false; isTtsEnabled.value = false;
Logger.info('发送ble音乐或者通话远端声音'); Logger.info('发送ble音乐或者通话远端声音');
@ -815,7 +923,24 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
/// 配置通话模式 /// 配置通话模式
Future<void> _configureCallMode() async { Future<void> _configureCallMode() async {
bleManager.openA2DPDecoder(); final List<String> callModeLanguages = [
sourceLanguageCode.value,
targetLanguageCode.value
];
Logger.info('1初始化AST服务,支持语言: $callModeLanguages'); //ast=asr+translate
await _astService.initialize(supportedLanguages: callModeLanguages);
isPreparing.value = true;
await bleManager.openA2DPDecoder();
int attempts = 0;
while (!bleManager.isCodecActive && attempts < 10) {
await Future.delayed(const Duration(milliseconds: 150));
attempts++;
}
isPreparing.value = false;
if (!bleManager.isCodecActive) {
Get.snackbar('tip'.tr, 'operationFailedPleaseRetry'.tr);
throw Exception('lxm--BLE编解码打开失败');
}
_audioSourceType = true; _audioSourceType = true;
isTtsEnabled.value = false; isTtsEnabled.value = false;
Logger.info('发送ble系统mic和dac(音乐或者通话远端)声音'); Logger.info('发送ble系统mic和dac(音乐或者通话远端)声音');
@ -845,53 +970,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;
} }
} }
@ -924,18 +1060,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 ||
@ -1279,6 +1418,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
/// 停止所有操作 /// 停止所有操作
Future<void> stopAll() async { Future<void> stopAll() async {
try { try {
isPreparing.value = false;
if (currentMode.value == "audioVideo" || currentMode.value == "call") { if (currentMode.value == "audioVideo" || currentMode.value == "call") {
bleManager.closeCodec(); bleManager.closeCodec();
Logger.info('关闭ble编解码器'); Logger.info('关闭ble编解码器');
@ -1466,27 +1606,34 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
} }
} }
/// 重新初始化ASR服务以支持新的语言 /// 重新初始化ASR服务
Future<void> _reinitializeAsrService() async { Future<void> _reinitializeAsrService() async {
try { try {
//1.统一先停止旧识别
await stopRecognition(); await stopRecognition();
final List<String> asrSupportedLanguages = [ late final List<String> asrSupportedLanguages;
sourceLanguageCode.value,
targetLanguageCode.value
];
Logger.info('重新初始化ASR服务,支持语言: $asrSupportedLanguages');
await _asrService.initialize(supportedLanguages: asrSupportedLanguages); if (currentMode.value == 'audioVideo') {
asrSupportedLanguages = [sourceLanguageCode.value];
var recognitionStream = await _asrService.recognizeCallback(); } else {
_recognitionSubscription?.cancel(); asrSupportedLanguages = [
_recognitionSubscription = sourceLanguageCode.value,
recognitionStream.listen(_handleRecognitionEvent); targetLanguageCode.value
];
}
if (currentMode.value == "call") { if (currentMode.value == "call") {
_initializeCallModeTranslationService(); _initializeCallModeTranslationService();
} else {
Logger.info('重新初始化ASR服务,支持语言: $asrSupportedLanguages');
await _asrService.initialize(supportedLanguages: asrSupportedLanguages);
var recognitionStream = await _asrService.recognizeCallback();
_recognitionSubscription?.cancel();
_recognitionSubscription =
recognitionStream.listen(_handleRecognitionEvent);
} }
Logger.info('ASR服务重新初始化成功'); Logger.info('ASR服务重新初始化成功');
} catch (e) { } catch (e) {

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();

2
lib/modules/translation/views/dialogs/translation_long_press_menu.dart

@ -174,7 +174,7 @@ class TranslationLongPressMenu {
'copySuccess'.tr, // 复制成功 'copySuccess'.tr, // 复制成功
backgroundColor: Colors.blue.withOpacity(0.8), backgroundColor: Colors.blue.withOpacity(0.8),
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
duration: const Duration(milliseconds: 1200), duration: const Duration(milliseconds: 1200),
icon: Icon( icon: Icon(
Icons.check_circle_outline, Icons.check_circle_outline,

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

@ -17,6 +17,7 @@ import '../../common/widgets/language_selector_bar.dart';
// 导入拆分后的组件 // 导入拆分后的组件
import 'widgets/face_to_face_layout.dart'; // 面对面翻译布局 import 'widgets/face_to_face_layout.dart'; // 面对面翻译布局
import 'widgets/draggable_floating_button.dart'; // 可拖拽的悬浮按钮 import 'widgets/draggable_floating_button.dart'; // 可拖拽的悬浮按钮
import 'widgets/translation_item_widget.dart'; // 翻译项组件
import 'dialogs/language_selection_dialog.dart'; // 语言选择对话框 import 'dialogs/language_selection_dialog.dart'; // 语言选择对话框
import 'dialogs/translation_mode_selector.dart'; // 翻译模式选择器 import 'dialogs/translation_mode_selector.dart'; // 翻译模式选择器
import 'dialogs/translation_long_press_menu.dart'; // 长按菜单 import 'dialogs/translation_long_press_menu.dart'; // 长按菜单
@ -27,6 +28,17 @@ import 'dialogs/translation_long_press_menu.dart'; // 长按菜单
class TranslationView extends GetView<TranslationController> { class TranslationView extends GetView<TranslationController> {
const TranslationView({Key? key}) : super(key: key); const TranslationView({Key? key}) : super(key: key);
// 本地交互状态:当前显示操作按钮的消息时间戳(不改动控制器)
static final Rx<DateTime?> _activeActionTimestamp = Rx<DateTime?>(null);
// 切换当前操作按钮展示的项(再次点击同一项则隐藏)
void _toggleActiveActionItem(TranslationItem item) {
final DateTime ts = item.timestamp;
final DateTime? current = _activeActionTimestamp.value;
_activeActionTimestamp.value =
(current != null && current.isAtSameMomentAs(ts)) ? null : ts;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark; final isDarkMode = Theme.of(context).brightness == Brightness.dark;
@ -68,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,
@ -137,7 +174,7 @@ class TranslationView extends GetView<TranslationController> {
size: 20.sp, size: 20.sp,
), ),
onPressed: () { onPressed: () {
// Stop recognition and TTS before navigating back // 点击返回时,停止所有语音识别和TTS
controller.stopAll(); controller.stopAll();
Get.back(); Get.back();
}, },
@ -198,28 +235,27 @@ class TranslationView extends GetView<TranslationController> {
return Row( return Row(
children: [ children: [
// 悬浮窗切换按钮(仅在支持的模式下显示) // 悬浮窗切换按钮(仅在支持的模式下显示)
if (Platform.isAndroid) Obx(() {
Obx(() { if (controller.currentMode.value == 'call' ||
if (controller.currentMode.value == 'call' || controller.currentMode.value == 'audioVideo') {
controller.currentMode.value == 'audioVideo') { return IconButton(
return IconButton( icon: Icon(
icon: Icon( controller.isFloatingWindowEnabled.value
controller.isFloatingWindowEnabled.value ? Icons.picture_in_picture
? Icons.picture_in_picture : Icons.picture_in_picture_alt_outlined,
: Icons.picture_in_picture_alt_outlined, color: controller.isFloatingWindowEnabled.value
color: controller.isFloatingWindowEnabled.value ? Colors.blue
? Colors.blue : (isDarkMode ? Colors.white : Colors.black),
: (isDarkMode ? Colors.white : Colors.black), size: 24.sp,
size: 24.sp, ),
), onPressed: controller.toggleFloatingWindow,
onPressed: controller.toggleFloatingWindow, tooltip: controller.isFloatingWindowEnabled.value
tooltip: controller.isFloatingWindowEnabled.value ? '禁用悬浮窗'
? '禁用悬浮窗' : '启用悬浮窗',
: '启用悬浮窗', );
); }
} return const SizedBox.shrink();
return const SizedBox.shrink(); }),
}),
IconButton( IconButton(
icon: Icon( icon: Icon(
Icons.radio_button_checked, Icons.radio_button_checked,
@ -271,10 +307,8 @@ class TranslationView extends GetView<TranslationController> {
children: [ children: [
// 统一通用语言选择栏组件 // 统一通用语言选择栏组件
LanguageSelectorBar( LanguageSelectorBar(
sourceText: () => sourceText: () => controller.sourceLanguage.value.split(' ')[0],
controller.sourceLanguage.value.split(' ')[0], targetText: () => controller.targetLanguage.value.split(' ')[0],
targetText: () =>
controller.targetLanguage.value.split(' ')[0],
onTapSource: () => onTapSource: () =>
LanguageSelectionDialog.show(true, isDarkMode), LanguageSelectionDialog.show(true, isDarkMode),
onTapTarget: () => onTapTarget: () =>
@ -286,9 +320,7 @@ class TranslationView extends GetView<TranslationController> {
? Icon( ? Icon(
Icons.phone_in_talk, Icons.phone_in_talk,
size: 20.sp, size: 20.sp,
color: isDarkMode color: isDarkMode ? Colors.white70 : Colors.grey[600],
? Colors.white70
: Colors.grey[600],
) )
: null, : null,
), ),
@ -396,128 +428,18 @@ class TranslationView extends GetView<TranslationController> {
final historyItem = controller.translationHistory[historyIndex]; final historyItem = controller.translationHistory[historyIndex];
// 构建项目widget // 构建项目widget
return _buildTranslationItemWidget(context, historyItem); return TranslationItemWidget(
item: historyItem,
activeActionTimestamp: _activeActionTimestamp,
onToggleActionItem: _toggleActiveActionItem,
onLongPressStart: _onLongPressTranslationItem,
onDeleteItemConfirm: _showDeleteItemConfirmDialog,
);
}, },
); );
}); });
} }
Widget _buildTranslationItemWidget(
BuildContext context, TranslationItem item) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
// 定义自适应颜色
final selfColor = isDarkMode
? Colors.blue[800]!.withOpacity(0.15) // 深色模式:深蓝+透明度
: Colors.blue[50]!; // 浅色模式:浅蓝
final otherColor = isDarkMode
? Colors.green[800]!.withOpacity(0.15) // 深色模式:深绿+透明度
: Colors.green[50]!; // 浅色模式:浅绿
return GestureDetector(
onLongPressStart: (details) => _onLongPressTranslationItem(
context,
isDarkMode,
details,
item,
),
child: Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: item.isIntermediate
? Colors.grey.withOpacity(isDarkMode ? 0.15 : 0.1) // 中间结果
: (item.sourceLanguageCode == controller.sourceLanguageCode.value
? selfColor // 自己说的话
: otherColor), // 对方说的话
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: item.isIntermediate
? Colors.grey.withOpacity(isDarkMode ? 0.3 : 0.2)
: Colors.transparent,
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 源文本和删除按钮行
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 源文本
Expanded(
child: Text(
item.sourceText,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).textTheme.bodyLarge?.color,
),
),
),
// 删除按钮 - 只对完成态记录显示,与源文本平齐
if (!item.isIntermediate)
GestureDetector(
onTap: () => _showDeleteItemConfirmDialog(item),
child: Container(
padding: EdgeInsets.all(4.w),
child: Icon(
Icons.delete_outline,
size: 18.sp,
color: Colors.grey[500],
),
),
),
],
),
if (item.translatedText.isNotEmpty) ...[
const SizedBox(height: 8),
// 翻译文本
Text(
item.translatedText,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).textTheme.bodyMedium?.color,
fontWeight: FontWeight.w500,
),
),
],
// 语言信息和时间戳
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'${item.sourceLanguageName.tr} → ${item.targetLanguageName.tr}',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
),
// 播放按钮
if (!item.isIntermediate)
GestureDetector(
onTap: () => controller.playTranslationItem(item),
child: Container(
padding: EdgeInsets.all(4.w),
child: Icon(
Icons.volume_up,
size: 20.sp,
color: Theme.of(context).colorScheme.primary,
),
),
),
],
),
],
),
),
);
}
Widget _buildControlBar(bool isDarkMode) { Widget _buildControlBar(bool isDarkMode) {
// 使用GlobalKey来获取控制栏的高度 // 使用GlobalKey来获取控制栏的高度
final controlBarKey = GlobalKey(); final controlBarKey = GlobalKey();
@ -545,134 +467,122 @@ class TranslationView extends GetView<TranslationController> {
), ),
], ],
), ),
child: Obx(() { child: Column(
return Column( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ // 原有的按钮区域
// 原有的按钮区域 Row(
if (controller.currentMode.value == 'faceToFace') mainAxisAlignment: MainAxisAlignment.center,
Row( children: [
mainAxisAlignment: MainAxisAlignment.center, // 主要控制按钮 - 开始/暂停语音识别
children: [ Obx(() {
// 耳机图标按钮 (说话者1)-显示源语言 final isRecognizing = controller.isRecognizing.value;
Obx(() => _buildHoldToTalkButton( final isPreparing = controller.isPreparing.value;
isDarkMode: isDarkMode, return GestureDetector(
speakerId: 1, onTap: () {
activeSpeaker: controller.activeSpeaker.value, if (isRecognizing) {
icon: Icons.mic_none, controller.stopAll();
label: controller.sourceLanguage.value } else {
.split(' ')[0] controller.startRecognition();
.tr, // 源语言名称 }
)), },
SizedBox(width: 16.w), child: AnimatedContainer(
// 手机图标按钮 (说话者2)-显示目标语言 duration: const Duration(milliseconds: 300),
Obx(() => _buildHoldToTalkButton( curve: Curves.easeInOut,
isDarkMode: isDarkMode, width: 60.w,
speakerId: 2, height: 60.w,
activeSpeaker: controller.activeSpeaker.value, transform: isPreparing
icon: Icons.mic_none, ? (Matrix4.identity()..scale(1.08))
label: controller.targetLanguage.value : Matrix4.identity(),
.split(' ')[0] decoration: BoxDecoration(
.tr, // 目标语言名称 color: isRecognizing
)), ? (isDarkMode ? Colors.red[700] : Colors.red)
], : (isDarkMode ? AppColors.primary : Colors.blue),
) shape: BoxShape.circle,
else boxShadow: isPreparing
Row( ? [
mainAxisAlignment: MainAxisAlignment.center, BoxShadow(
children: [ color: (isDarkMode
// 主要控制按钮 - 开始/暂停语音识别 ? AppColors.primary
Obx(() { : Colors.blue)
final isRecognizing = controller.isRecognizing.value; .withOpacity(0.5),
return GestureDetector( blurRadius: 12,
onTap: () { spreadRadius: 2,
if (isRecognizing) { )
controller.stopAll(); ]
} else { : [],
controller.startRecognition(); ),
} child: Icon(
}, isRecognizing ? Icons.pause : Icons.play_arrow,
child: Container( color: Colors.white,
width: 60.w, size: 30.sp,
height: 60.w, ),
decoration: BoxDecoration( ),
color: isRecognizing );
? (isDarkMode ? Colors.red[700] : Colors.red) }),
: (isDarkMode ? AppColors.primary : Colors.blue), ],
shape: BoxShape.circle, ),
),
child: Icon(
isRecognizing ? Icons.pause : Icons.play_arrow,
color: Colors.white,
size: 30.sp,
),
),
);
}),
],
),
// 添加间距 // 添加间距
//SizedBox(height: 16.h), //SizedBox(height: 16.h),
// 语音识别结果显示框 // 语音识别结果显示框
// Container( // Container(
// width: double.infinity, // width: double.infinity,
// padding: EdgeInsets.all(12.w), // padding: EdgeInsets.all(12.w),
// decoration: BoxDecoration( // decoration: BoxDecoration(
// color: isDarkMode ? Colors.grey[800] : Colors.grey[100], // color: isDarkMode ? Colors.grey[800] : Colors.grey[100],
// borderRadius: BorderRadius.circular(8.r), // borderRadius: BorderRadius.circular(8.r),
// border: Border.all( // border: Border.all(
// color: isDarkMode ? Colors.grey[600]! : Colors.grey[300]!, // color: isDarkMode ? Colors.grey[600]! : Colors.grey[300]!,
// width: 1, // width: 1,
// ), // ),
// ), // ),
// child: Obx(() { // child: Obx(() {
// // 显示最新的语音识别结果 // // 显示最新的语音识别结果
// final recognitionText = controller.latestRecognitionText.value; // final recognitionText = controller.latestRecognitionText.value;
// final detectedLanguage = controller.detectedLanguageCode1.value; // final detectedLanguage = controller.detectedLanguageCode1.value;
// return Column( // return Column(
// crossAxisAlignment: CrossAxisAlignment.start, // crossAxisAlignment: CrossAxisAlignment.start,
// children: [ // children: [
// // 标题 // // 标题
// Text( // Text(
// '麦头语音识别结果', // '麦头语音识别结果',
// style: TextStyle( // style: TextStyle(
// fontSize: 12.sp, // fontSize: 12.sp,
// color: isDarkMode ? Colors.grey[400] : Colors.grey[600], // color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
// fontWeight: FontWeight.w500, // fontWeight: FontWeight.w500,
// ), // ),
// ), // ),
// SizedBox(height: 4.h), // SizedBox(height: 4.h),
// // 识别文本 - 这里显示 finalResult1 的内容 // // 识别文本 - 这里显示 finalResult1 的内容
// Text( // Text(
// recognitionText.isEmpty ? '等待语音输入...' : recognitionText, // recognitionText.isEmpty ? '等待语音输入...' : recognitionText,
// style: TextStyle( // style: TextStyle(
// fontSize: 14.sp, // fontSize: 14.sp,
// color: isDarkMode ? Colors.white : Colors.black87, // color: isDarkMode ? Colors.white : Colors.black87,
// height: 1.3, // height: 1.3,
// ), // ),
// ), // ),
// // 检测到的语言(如果有) // // 检测到的语言(如果有)
// if (detectedLanguage.isNotEmpty) ...[ // if (detectedLanguage.isNotEmpty) ...[
// SizedBox(height: 4.h), // SizedBox(height: 4.h),
// Text( // Text(
// '检测语言: $detectedLanguage', // '检测语言: $detectedLanguage',
// style: TextStyle( // style: TextStyle(
// fontSize: 10.sp, // fontSize: 10.sp,
// color: // color:
// isDarkMode ? Colors.grey[500] : Colors.grey[500], // isDarkMode ? Colors.grey[500] : Colors.grey[500],
// ), // ),
// ), // ),
// ], // ],
// ], // ],
// ); // );
// }), // }),
// ), // ),
], ],
); ),
}),
); );
} }
@ -781,10 +691,10 @@ class TranslationView extends GetView<TranslationController> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
SizedBox( SizedBox(
width: 24, width: 24.w,
height: 24, height: 24.h,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2.5, strokeWidth: 2.5.w,
valueColor: const AlwaysStoppedAnimation<Color>( valueColor: const AlwaysStoppedAnimation<Color>(
AppColors.primary, AppColors.primary,
), ),
@ -792,7 +702,7 @@ class TranslationView extends GetView<TranslationController> {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Text( Text(
'正在加载历史记录...', 'loading'.tr, //加载中
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: Colors.grey[600], color: Colors.grey[600],
@ -828,7 +738,7 @@ class TranslationView extends GetView<TranslationController> {
), ),
SizedBox(width: 8.w), SizedBox(width: 8.w),
Text( Text(
'加载失败,点击重试', 'loadFailedClickRetry'.tr,
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: Colors.red[600], color: Colors.red[600],
@ -862,7 +772,7 @@ class TranslationView extends GetView<TranslationController> {
), ),
SizedBox(width: 4.w), SizedBox(width: 4.w),
Text( Text(
'向上滑动查看更多历史', 'swipeUpForMore'.tr,
style: TextStyle( style: TextStyle(
fontSize: 12.sp, fontSize: 12.sp,
color: Colors.grey[600], color: Colors.grey[600],

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,

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

@ -0,0 +1,195 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../controllers/translation_controller.dart';
import '../../models/translation_models.dart';
import '../dialogs/translation_long_press_menu.dart';
class TranslationItemWidget extends GetView<TranslationController> {
// 传入的翻译条目
final TranslationItem item;
// 传入用于控制操作按钮显隐的时间戳状态
final Rx<DateTime?> activeActionTimestamp;
// 点击气泡时切换操作按钮显隐
final void Function(TranslationItem item) onToggleActionItem;
// 长按时弹出菜单(需要上下文、暗色模式、手势详情、条目)
final void Function(
BuildContext context,
bool isDarkMode,
LongPressStartDetails details,
TranslationItem item,
) onLongPressStart;
// 删除按钮点击时的确认对话框
final void Function(TranslationItem item) onDeleteItemConfirm;
const TranslationItemWidget({
Key? key,
required this.item,
required this.activeActionTimestamp,
required this.onToggleActionItem,
required this.onLongPressStart,
required this.onDeleteItemConfirm,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
// 定义自适应颜色(与原实现保持一致)
final selfColor =
isDarkMode ? Colors.blue[800]!.withOpacity(0.15) : Colors.blue[50]!;
final otherColor =
isDarkMode ? Colors.green[800]!.withOpacity(0.15) : Colors.green[50]!;
// 判断是否是己方的翻译项(依据控制器当前源语言)
final bool isSelf =
item.sourceLanguageCode == controller.sourceLanguageCode.value;
// 新增:通话模式标记,用于控制右侧显示与隐藏中间态
final bool isCallMode = controller.currentMode.value == 'call';
return GestureDetector(
onTap: () => onToggleActionItem(item), // 触碰气泡时切换按钮显隐
onLongPressStart: (details) =>
onLongPressStart(context, isDarkMode, details, item),
child: Align(
alignment: isCallMode
? (isSelf ? Alignment.centerLeft : Alignment.centerRight)
: item.isIntermediate
? Alignment.center
: (isSelf ? Alignment.centerLeft : Alignment.centerRight),
child: Container(
margin: EdgeInsets.only(bottom: 8.h),
padding: EdgeInsets.all(12.w),
constraints: BoxConstraints(maxWidth: 0.8.sw, minWidth: 0.35.sw),
decoration: BoxDecoration(
color: item.isIntermediate
? Colors.grey.withOpacity(isDarkMode ? 0.15 : 0.1) // 中间结果
: (isSelf ? selfColor : otherColor), // 自己 / 对方
borderRadius: BorderRadius.circular(12.r),
border: Border.all(
color: item.isIntermediate
? Colors.grey.withOpacity(isDarkMode ? 0.3 : 0.2)
: Colors.transparent,
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 源文本
Text(
item.sourceText,
style: TextStyle(
fontSize: 16.sp,
color: Theme.of(context).textTheme.bodyLarge?.color,
),
),
SizedBox(height: 8.h),
// 分隔线 + 译文
Container(
padding: EdgeInsets.only(top: 8.h),
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: item.isIntermediate
? (isDarkMode
? Colors.grey[400]!.withOpacity(0.15)
: Colors.grey[400]!.withOpacity(0.25))
: (isSelf
? (isDarkMode
? Colors.blue[300]!.withOpacity(0.15)
: Colors.blue[300]!.withOpacity(0.25))
: (isDarkMode
? Colors.green[300]!.withOpacity(0.15)
: Colors.green[300]!.withOpacity(0.25))),
width: 1,
),
),
),
child: Text(
item.translatedText,
style: TextStyle(
fontSize: 16.sp,
color: Theme.of(context).textTheme.bodyMedium?.color,
fontWeight: FontWeight.w500,
),
),
),
// 操作按钮:复制 / 播放 / 删除(仅非中间态且显隐状态为激活时显示)
Obx(() {
final bool isActive = activeActionTimestamp.value
?.isAtSameMomentAs(item.timestamp) ??
false;
if (!item.isIntermediate && isActive) {
return Column(
children: [
SizedBox(height: 8.h),
Row(
mainAxisSize: MainAxisSize.min,
children: [
// 复制按钮(复用长按菜单的复制逻辑)
GestureDetector(
onTap: () =>
TranslationLongPressMenu.copyTranslationItem(
item),
child: Container(
padding: EdgeInsets.all(4.w),
child: Tooltip(
message: 'copy'.tr, // 翻译键代表的中文文本内容
child: Icon(
Icons.copy,
size: 20.sp,
color: Theme.of(context).colorScheme.primary,
),
),
),
),
SizedBox(width: 8.w),
// 播放按钮(调用控制器原有方法)
GestureDetector(
onTap: () => controller.playTranslationItem(item),
child: Container(
padding: EdgeInsets.all(4.w),
child: Icon(
Icons.volume_up,
size: 20.sp,
color: Theme.of(context).colorScheme.primary,
),
),
),
SizedBox(width: 8.w),
// 删除按钮(弹出确认)
GestureDetector(
onTap: () => onDeleteItemConfirm(item),
child: Container(
padding: EdgeInsets.all(4.w),
child: Tooltip(
message: 'delete'.tr, // 翻译键代表的中文文本内容
child: Icon(
Icons.delete_outline,
size: 20.sp,
color: Colors.grey[500],
),
),
),
),
],
)
],
);
} else {
return const SizedBox.shrink();
}
}),
],
),
),
),
);
}
}

1
lib/routes/app_routes.dart

@ -19,6 +19,7 @@ abstract class Routes {
static const settings = '/settings'; // 系统设置 static const settings = '/settings'; // 系统设置
static const permissions = '/permissions'; // 权限管理 static const permissions = '/permissions'; // 权限管理
static const recording = '/recording'; // 录音功能 static const recording = '/recording'; // 录音功能
static const eqSettings = '/eq_settings'; // 设备设置
// 会议相关 // 会议相关
static const meeting = '/meeting'; static const meeting = '/meeting';

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

@ -183,8 +183,13 @@ class BleCommandSender {
} }
/** /**
* 处理设备响应数据 - 从BleService移动过来 * 处理设备响应数据(统一兼容失败无长度的场景)
* @param data 响应数据 *
* 帧格式通常为: [HEADER(0)=0xBB][CMD(1)][LEN/STATUS(2)][DATA(len)][CRC(最后1字节)]
* - 当 LEN/STATUS 为 0(例如收到 0xA2, 0x00),表示该命令执行失败,直接回调失败并返回。
* - 正常响应时进行 CRC 校验(若帧长度不足则跳过校验并告警),拷贝数据使用安全长度以避免越界。
*
* @param data 设备返回的原始字节数组
*/ */
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
@ -200,63 +205,64 @@ class BleCommandSender {
val header = data[0] // 0xBB val header = data[0] // 0xBB
val cmdType = data[1].toInt() and 0xFF val cmdType = data[1].toInt() and 0xFF
val lenByte = data[2].toInt() and 0xFF
// 判断命令失败响应(长度为0)
if (data.size == 4 && data[2].toInt() and 0xFF == 0) {
Log.w(TAG, "命令 0x${cmdType.toString(16)} 执行失败")
// 创建失败结果Map // 正常响应处理:尽量进行 CRC 校验
val resultMap = mapOf( var crcOk = true
"success" to false, if (data.size >= 4) {
"command" to cmdType, val payload = data.sliceArray(0 until data.size - 1)
"rawData" to byteArrayOf() val calculatedCrc = calculateCrc8Maxim(payload, payload.size)
) val receivedCrc = data[data.size - 1].toInt() and 0xFF
callback?.notifyDeviceInfoReceived(cmdType, resultMap) crcOk = (calculatedCrc.toInt() and 0xFF) == receivedCrc
// 通知指令发送器处理响应 if (!crcOk) {
onCommandResponse(cmdType) Log.e(
return TAG,
} "设备响应CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}"
)
// 正常响应处理 return
val length = data[2].toInt() and 0xFF }
} else {
// 检查数据长度 Log.w(TAG, "设备响应长度不足以包含CRC,跳过CRC校验")
if (data.size != length + 4) { // 帧头 + 命令 + 长度 + 数据 + CRC
Log.e(TAG, "设备响应数据长度不匹配: 期望=${length + 4}, 实际=${data.size}")
return
} }
// 统一失败判定
if (lenByte != data.size-4) {
Log.w(TAG, "命令 0x${cmdType.toString(16)} 执行失败(无数据长度)")
val resultMap = mapOf(
"success" to true,
"command" to cmdType,
"rawData" to byteArrayOf(data[2], data[data.size - 2]) // 取第二位和倒数第二位
)
callback?.notifyDeviceInfoReceived(cmdType, resultMap)
onCommandResponse(cmdType)
return
}
// 安全提取响应数据,避免越界
val actualPayloadLen = (data.size - 4).coerceAtLeast(0)
val copyLen = minOf(lenByte, actualPayloadLen)
val responseData = if (copyLen > 0) ByteArray(copyLen) else ByteArray(0)
// 计算CRC校验 - 修正:计算除最后一个CRC字节外的所有字节 if (copyLen < lenByte) {
val payload = data.sliceArray(0 until data.size - 1) Log.w(
val calculatedCrc = calculateCrc8Maxim(payload, payload.size)
val receivedCrc = data[data.size - 1].toInt() and 0xFF
if (calculatedCrc.toInt() and 0xFF != receivedCrc) {
Log.e(
TAG, TAG,
"设备响应CRC校验失败: 计算=${calculatedCrc.toInt() and 0xFF}, 接收=${receivedCrc}" "响应长度字段(${lenByte})与实际负载长度(${actualPayloadLen})不一致,按实际长度处理"
) )
return
} }
// 提取响应数据 if (copyLen > 0) {
val responseData = if (length > 0) ByteArray(length) else ByteArray(0) System.arraycopy(data, 3, responseData, 0, copyLen)
if (length > 0) {
System.arraycopy(data, 3, responseData, 0, length)
} }
Log.d( Log.d(
TAG, TAG,
"收到设备响应: CMD=0x${cmdType.toString(16)}, 数据=${ "收到设备响应: CMD=0x${cmdType.toString(16)}, 数据=${
responseData.joinToString(", ") { responseData.joinToString(", ") { "0x${(it.toInt() and 0xFF).toString(16)}" }
"0x${
(it.toInt() and 0xFF).toString(16)
}"
}
}" }"
) )
// 通知指令发送器处理响应
onCommandResponse(cmdType) onCommandResponse(cmdType)
// 根据命令类型处理不同的响应
when (cmdType) { when (cmdType) {
BleConst.CMD_GET_VERSION -> { BleConst.CMD_GET_VERSION -> {
val resultMap = mutableMapOf<String, Any>( val resultMap = mutableMapOf<String, Any>(

3
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleConst.kt

@ -21,7 +21,8 @@ object BleConst {
/** 通话写入音频特征UUID - 文档中定义为0000ABC1-0001-1111-2222-123456789ABC */ /** 通话写入音频特征UUID - 文档中定义为0000ABC1-0001-1111-2222-123456789ABC */
val CALL_WRITE_AUDIO_CHAR_UUID: UUID = UUID.fromString("0000ABC1-0001-1111-2222-123456789ABC") val CALL_WRITE_AUDIO_CHAR_UUID: UUID = UUID.fromString("0000ABC1-0001-1111-2222-123456789ABC")
/** 通话接收音频特征UUID - 文档中定义为0000ABC2-0001-1111-2222-123456789ABC */
val CALL_RECEIVE_AUDIO_CHAR_UUID: UUID = UUID.fromString("0000ABC2-0001-1111-2222-123456789ABC")
/** 音频服务UUID - 文档中定义为00001801-0000-1000-8000-00805f9b34fb */ /** 音频服务UUID - 文档中定义为00001801-0000-1000-8000-00805f9b34fb */
val AUDIO_SERVICE_UUID: UUID = UUID.fromString("0000ae00-0000-1000-8000-00805f9b34fb") val AUDIO_SERVICE_UUID: UUID = UUID.fromString("0000ae00-0000-1000-8000-00805f9b34fb")

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

@ -75,6 +75,7 @@ object BleService {
private var notifyChar: BluetoothGattCharacteristic? = null private var notifyChar: BluetoothGattCharacteristic? = null
private var writeChar: BluetoothGattCharacteristic? = null private var writeChar: BluetoothGattCharacteristic? = null
private var audioChar: BluetoothGattCharacteristic? = null private var audioChar: BluetoothGattCharacteristic? = null
private var callAudioChar: BluetoothGattCharacteristic? = null
private var callWriteChar: BluetoothGattCharacteristic? = null private var callWriteChar: BluetoothGattCharacteristic? = null
var recordfile: RecordingFile? = null var recordfile: RecordingFile? = null
var recordfile1: RecordingFile? = null var recordfile1: RecordingFile? = null
@ -702,6 +703,7 @@ object BleService {
notifyChar = null notifyChar = null
writeChar = null writeChar = null
audioChar = null audioChar = null
callAudioChar = null
callWriteChar = null callWriteChar = null
} }
} }
@ -849,6 +851,7 @@ object BleService {
// 获取通话音频服务特征 // 获取通话音频服务特征
val callAudioSvc = g.getService(BleConst.CALL_AUDIO_SERVICE_UUID) val callAudioSvc = g.getService(BleConst.CALL_AUDIO_SERVICE_UUID)
callWriteChar = callAudioSvc?.getCharacteristic(BleConst.CALL_WRITE_AUDIO_CHAR_UUID) callWriteChar = callAudioSvc?.getCharacteristic(BleConst.CALL_WRITE_AUDIO_CHAR_UUID)
callAudioChar = callAudioSvc?.getCharacteristic(BleConst.CALL_RECEIVE_AUDIO_CHAR_UUID)
if (callWriteChar == null) { if (callWriteChar == null) {
Log.e(TAG, "未找到通话音频主服务所需特征") Log.e(TAG, "未找到通话音频主服务所需特征")
updateConnectionState(BleConst.STATE_ERROR) updateConnectionState(BleConst.STATE_ERROR)
@ -860,12 +863,15 @@ object BleService {
callWriteChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE callWriteChar?.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
//要先设置音频服务的通知,否则接收不到 //要先设置音频服务的通知,否则接收不到
// 设置音频服务的通知(如果存在) // 设置音频服务的通知(如果存在)
if (audioChar != null) { if (callAudioChar != null && audioChar != null) {
setupNotifications(g, callAudioChar)
setupNotifications(g, audioChar) setupNotifications(g, audioChar)
Log.i(TAG, "音频服务特征找到并设置通知")
Log.i(TAG, "通话音频服务特征找到并设置通知")
} else { } else {
Log.w(TAG, "音频服务特征未找到") Log.w(TAG, "通话音频服务特征未找到")
} }
setupNotifications(g, notifyChar) setupNotifications(g, notifyChar)
@ -887,6 +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 -> {
// 统计接收到的字节数
bytesReceivedInCurrentSecond += data.size
processAudioData(data)
Log.i(TAG, "lxm--收到通话音频特征数据, 字节数=${data.size}")
} }
// 通知特征数据(命令和控制) // 通知特征数据(命令和控制)
BleConst.NOTIFY_CHAR_UUID -> { BleConst.NOTIFY_CHAR_UUID -> {

68
local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/OpusAudioManager.kt

@ -82,11 +82,13 @@ class OpusAudioManager {
// ====================================================================================================== // ======================================================================================================
// Opus解码器实例 // Opus解码器实例
private var opusManager: OpusManager? = null //解码对象
private var option: OpusOption? = null private var decodeOpusManager: OpusManager? = null
private var decodeOption: OpusOption? = null
private var opusManager1: OpusManager? = null //编码对象
private var option1: OpusOption? = null private var encodeOpusManager: OpusManager? = null
private var encodeOption: OpusOption? = null
private val mainHandler = Handler(Looper.getMainLooper()) private val mainHandler = Handler(Looper.getMainLooper())
// 解码音频数据队列和处理线程 // 解码音频数据队列和处理线程
@ -124,11 +126,11 @@ class OpusAudioManager {
try { try {
// 初始化OpusManager和OpusOption // 初始化OpusManager和OpusOption
opusManager = OpusManager() decodeOpusManager = OpusManager()
option = OpusOption() decodeOption = OpusOption()
opusManager1 = OpusManager() encodeOpusManager = OpusManager()
option1 = OpusOption() encodeOption = OpusOption()
// 启动队列处理 // 启动队列处理
startAudioQueueProcessing() startAudioQueueProcessing()
@ -178,10 +180,10 @@ class OpusAudioManager {
audioSendQueue.clear() audioSendQueue.clear()
// 清理Opus相关资源 // 清理Opus相关资源
opusManager = null decodeOpusManager = null
option = null decodeOption = null
opusManager1 = null encodeOpusManager = null
option1 = null encodeOption = null
callback = null callback = null
isInitialized = false isInitialized = false
@ -206,14 +208,14 @@ class OpusAudioManager {
sampleRate: Int = 16000, sampleRate: Int = 16000,
packetSize: Int = 40 packetSize: Int = 40
): Boolean { ): Boolean {
if (!isInitialized || opusManager == null) { if (!isInitialized || decodeOpusManager == null) {
Log.e(TAG, "OpusManager未初始化,无法开始解码") Log.e(TAG, "OpusManager未初始化,无法开始解码")
return false return false
} }
// 如果已经在解码流,先停止 // 如果已经在解码流,先停止
if (opusManager?.isDecodeStream == true) { if (decodeOpusManager?.isDecodeStream == true) {
opusManager?.stopDecodeStream() decodeOpusManager?.stopDecodeStream()
Log.d(TAG, "已停止正在进行的Opus解码流") Log.d(TAG, "已停止正在进行的Opus解码流")
} }
@ -224,7 +226,7 @@ class OpusAudioManager {
} }
// 配置解码参数 // 配置解码参数
option?.apply { decodeOption?.apply {
setHasHead(hasHeader) setHasHead(hasHeader)
setChannel(channel) setChannel(channel)
setSampleRate(sampleRate) setSampleRate(sampleRate)
@ -234,10 +236,10 @@ class OpusAudioManager {
Log.d(TAG, "准备开始Opus数据流解码, 参数: hasHeader=$hasHeader, channel=$channel, sampleRate=$sampleRate, packetSize=$packetSize") Log.d(TAG, "准备开始Opus数据流解码, 参数: hasHeader=$hasHeader, channel=$channel, sampleRate=$sampleRate, packetSize=$packetSize")
try { try {
opusManager?.startDecodeStream(option, object : OnDecodeStreamCallback { decodeOpusManager?.startDecodeStream(decodeOption, object : OnDecodeStreamCallback {
override fun onDecodeStream(data: ByteArray?) { override fun onDecodeStream(data: ByteArray?) {
if (data != null) { if (data != null) {
val channelCount = option?.getChannel() ?: 1 val channelCount = decodeOption?.getChannel() ?: 1
if (channelCount >= 1) { if (channelCount >= 1) {
callback?.onAudioDataDecoded(data, channelCount) callback?.onAudioDataDecoded(data, channelCount)
} else { } else {
@ -276,8 +278,8 @@ class OpusAudioManager {
* @return 是否成功停止解码 * @return 是否成功停止解码
*/ */
fun stopDecodeStream(): Boolean { fun stopDecodeStream(): Boolean {
if (opusManager?.isDecodeStream == true) { if (decodeOpusManager?.isDecodeStream == true) {
opusManager?.stopDecodeStream() decodeOpusManager?.stopDecodeStream()
// 清理音频数据缓存 // 清理音频数据缓存
synchronized(audioDataBuffer) { synchronized(audioDataBuffer) {
@ -303,7 +305,7 @@ class OpusAudioManager {
return return
} }
if (opusManager?.isDecodeStream == true) { if (decodeOpusManager?.isDecodeStream == true) {
// 将数据加入队列进行异步处理 // 将数据加入队列进行异步处理
audioDataQueue.offer(data) audioDataQueue.offer(data)
} else { } else {
@ -320,19 +322,19 @@ class OpusAudioManager {
* @return 是否成功开始编码 * @return 是否成功开始编码
*/ */
fun startEncodeStream(): Boolean { fun startEncodeStream(): Boolean {
if (!isInitialized || opusManager1 == null) { if (!isInitialized || encodeOpusManager == null) {
Log.e(TAG, "OpusManager未初始化,无法开始编码") Log.e(TAG, "OpusManager未初始化,无法开始编码")
return false return false
} }
// 如果已经在编码流,先停止 // 如果已经在编码流,先停止
if (opusManager1?.isEncodeStream == true) { if (encodeOpusManager?.isEncodeStream == true) {
Log.d(TAG, "Opus编码流已在运行") Log.d(TAG, "Opus编码流已在运行")
return true return true
} }
// 配置解码参数 // 配置解码参数
option1?.apply { encodeOption?.apply {
setHasHead(false) setHasHead(false)
setChannel(1) setChannel(1)
setSampleRate(16000) setSampleRate(16000)
@ -340,7 +342,7 @@ class OpusAudioManager {
} }
try { try {
Log.d(TAG, "准备开始Opus数据流编码") Log.d(TAG, "准备开始Opus数据流编码")
opusManager1?.startEncodeStream(object : OnEncodeStreamCallback { encodeOpusManager?.startEncodeStream(object : OnEncodeStreamCallback {
override fun onEncodeStream(data: ByteArray?) { override fun onEncodeStream(data: ByteArray?) {
Log.d(TAG, "Opus编码回调收到数据,大小: ${data?.size ?: 0} bytes") Log.d(TAG, "Opus编码回调收到数据,大小: ${data?.size ?: 0} bytes")
if (data != null) { if (data != null) {
@ -386,8 +388,8 @@ class OpusAudioManager {
* @return 是否成功停止编码 * @return 是否成功停止编码
*/ */
fun stopEncodeStream(): Boolean { fun stopEncodeStream(): Boolean {
if (opusManager1?.isEncodeStream == true) { if (encodeOpusManager?.isEncodeStream == true) {
opusManager1?.stopEncodeStream() encodeOpusManager?.stopEncodeStream()
stopAudioSendThread() // 停止音频发送线程 stopAudioSendThread() // 停止音频发送线程
Log.i(TAG, "已停止Opus数据流编码") Log.i(TAG, "已停止Opus数据流编码")
callback?.onEncodeStreamStateChanged(false) callback?.onEncodeStreamStateChanged(false)
@ -404,16 +406,16 @@ class OpusAudioManager {
* @param data 音频数据字节数组,通常为PCM格式 * @param data 音频数据字节数组,通常为PCM格式
*/ */
fun writeExternalAudioData(data: ByteArray) { fun writeExternalAudioData(data: ByteArray) {
if (!isInitialized || opusManager1 == null) { if (!isInitialized || encodeOpusManager == null) {
Log.e(TAG, "OpusManager未初始化") Log.e(TAG, "OpusManager未初始化")
return return
} }
// 如果已经在编码流中,直接写入数据 // 如果已经在编码流中,直接写入数据
if (opusManager1?.isEncodeStream == true) { if (encodeOpusManager?.isEncodeStream == true) {
Log.d(TAG, "正在进行Opus编码流,写入音频数据") Log.d(TAG, "正在进行Opus编码流,写入音频数据")
// 将外部音频数据写入编码流 // 将外部音频数据写入编码流
opusManager1?.writeEncodeStream(data) encodeOpusManager?.writeEncodeStream(data)
} else { } else {
Log.w(TAG, "Opus编码流未启动,无法写入音频数据") Log.w(TAG, "Opus编码流未启动,无法写入音频数据")
} }
@ -428,7 +430,7 @@ class OpusAudioManager {
* @return 是否正在解码 * @return 是否正在解码
*/ */
fun isDecoding(): Boolean { fun isDecoding(): Boolean {
return opusManager?.isDecodeStream == true return decodeOpusManager?.isDecodeStream == true
} }
/** /**
@ -436,7 +438,7 @@ class OpusAudioManager {
* @return 是否正在编码 * @return 是否正在编码
*/ */
fun isEncoding(): Boolean { fun isEncoding(): Boolean {
return opusManager1?.isEncodeStream == true return encodeOpusManager?.isEncodeStream == true
} }
/** /**
@ -488,7 +490,7 @@ class OpusAudioManager {
val audioData = audioDataQueue.take() // 阻塞等待 val audioData = audioDataQueue.take() // 阻塞等待
// 将音频数据写入Opus解码流 // 将音频数据写入Opus解码流
opusManager?.writeAudioStream(audioData) decodeOpusManager?.writeAudioStream(audioData)
} catch (e: InterruptedException) { } catch (e: InterruptedException) {
Log.d(TAG, "音频队列处理线程被中断") Log.d(TAG, "音频队列处理线程被中断")

2
local_plugins/ble_service/ios/ble_service/Sources/ble_service/BleConst.swift

@ -26,6 +26,8 @@ class BleConst {
/** 通话写入音频特征UUID - 文档中定义为0000ABC1-0001-1111-2222-123456789ABC */ /** 通话写入音频特征UUID - 文档中定义为0000ABC1-0001-1111-2222-123456789ABC */
static let CALL_WRITE_AUDIO_CHAR_UUID = CBUUID(string: "0000abc1-0001-1111-2222-123456789abc") static let CALL_WRITE_AUDIO_CHAR_UUID = CBUUID(string: "0000abc1-0001-1111-2222-123456789abc")
/** 通话接收音频特征UUID - 文档中定义为0000ABC2-0001-1111-2222-123456789ABC */
static let CALL_RECEIVE_AUDIO_CHAR_UUID = CBUUID(string: "0000abc2-0001-1111-2222-123456789abc")
/** 客户端特征配置描述符UUID */ /** 客户端特征配置描述符UUID */
static let CLIENT_CHAR_CONFIG_UUID = CBUUID(string: "00002902-0000-1000-8000-00805f9b34fb") static let CLIENT_CHAR_CONFIG_UUID = CBUUID(string: "00002902-0000-1000-8000-00805f9b34fb")

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

@ -41,11 +41,12 @@ public class BleService: NSObject {
private var centralManager: CBCentralManager? private var centralManager: CBCentralManager?
private var peripheralDevice: CBPeripheral? private var peripheralDevice: CBPeripheral?
private var writeCharacteristic: CBCharacteristic? private var writeCharacteristic: CBCharacteristic?
private var callAudioCharacteristic: CBCharacteristic? private var callWriteCharacteristic: CBCharacteristic?
private var notifyCharacteristic: CBCharacteristic? private var notifyCharacteristic: CBCharacteristic?
private var audioCharacteristic: CBCharacteristic? private var audioCharacteristic: CBCharacteristic?
private var callAudioCharacteristic: CBCharacteristic?
// 添加录制文件管理器 // 添加录制文件管理器
private var recordingFile: RecordingFile? private var recordingFile: RecordingFile?
@ -281,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)
} }
} }
@ -318,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() {
@ -418,10 +442,11 @@ private var cmdReplyType: UInt8 = 0
// 清空特征引用,强制重新发现 // 清空特征引用,强制重新发现
writeCharacteristic = nil writeCharacteristic = nil
callAudioCharacteristic = nil callWriteCharacteristic = nil
notifyCharacteristic = nil notifyCharacteristic = nil
audioCharacteristic = nil audioCharacteristic = nil
callAudioCharacteristic = nil
os_log("🔍 恢复会话时重新发现服务和特征", log: logger, type: .info) os_log("🔍 恢复会话时重新发现服务和特征", log: logger, type: .info)
peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID, BleConst.CALL_AUDIO_SERVICE_UUID]) peripheral.discoverServices([BleConst.PRIMARY_SERVICE_UUID, BleConst.AUDIO_SERVICE_UUID, BleConst.CALL_AUDIO_SERVICE_UUID])
@ -866,22 +891,27 @@ private var cmdReplyType: UInt8 = 0
} }
/// 处理接收到的响应数据 /// 处理接收到的响应数据
///
/// - 功能:
/// - 解析设备响应帧,校验头、长度与 CRC
/// - 与 Android 保持一致的失败判定:当长度字段与实际长度不匹配时,上报 rawData = [length, data[data.count-2]]
/// - 在 CMD_CONTROL_CODEC 分支对齐安卓对错误响应的处理逻辑
private func handleResponseData(_ data: Data) { private func handleResponseData(_ data: Data) {
guard let characteristic = writeCharacteristic, guard let characteristic = writeCharacteristic,
connectionState == BleConst.STATE_CONNECTED else { connectionState == BleConst.STATE_CONNECTED else {
os_log(" 设备未连接", log: logger, type: .error) os_log(" 设备未连接", log: logger, type: .error)
return return
} }
guard data.count >= 4 else { guard data.count >= 4 else {
os_log("响应数据格式错误:数据长度过短", log: logger, type: .error) os_log("响应数据格式错误:数据长度过短", log: logger, type: .error)
return return
} }
// 提取帧信息 // 提取帧信息
let header = data[0] let header = data[0]
let cmd = data[1] let cmd = data[1]
let length = data[2] let length = data[2]
// 检查帧头,确保是设备响应 // 检查帧头,确保是设备响应
guard header == BleConst.DEVICE_RESPONSE_HEADER else { guard header == BleConst.DEVICE_RESPONSE_HEADER else {
// 如果是设备主动上报 // 如果是设备主动上报
@ -890,35 +920,59 @@ private var cmdReplyType: UInt8 = 0
} }
return return
} }
// 记录响应详情 // 记录响应详情
var hexString = data.map { String(format: "0x%02X", $0) }.joined(separator: ", ") var hexString = data.map { String(format: "0x%02X", $0) }.joined(separator: ", ")
os_log("收到设备响应: %@", log: logger, type: .debug, hexString) os_log("收到设备响应: %@", log: logger, type: .debug, hexString)
// 检查数据长度 // 检查数据长度(与 Android 统一失败判定保持一致)
if data.count != Int(length) + 4 { // 帧头 + 命令 + 长度 + 数据 + CRC if data.count != Int(length) + 4 { // 帧头 + 命令 + 长度 + 数据 + CRC
os_log("设备响应数据长度不匹配: 期望=%d, 实际=%d", log: logger, type: .error, Int(length) + 4, data.count) os_log("设备响应数据长度不匹配: 期望=%d, 实际=%d", log: logger, type: .error, Int(length) + 4, data.count)
// 与 Android 保持一致:构造 rawData 为 [length, 倒数第二位(payload 最后一位)]
var raw: [UInt8] = []
raw.append(length)
if data.count >= 2 {
raw.append(data[data.count - 2])
}
let resultMap: [String: Any] = [
"success": true, // 与 Android 对齐(Dart 层按 rawData 判断)
"command": Int(cmd),
"rawData": Data(raw)
]
// 推进命令队列(与后续正常路径一致)
isReply = true
cmdReplyType = cmd
if !commandQueue.isEmpty {
let nextCmd = commandQueue.removeFirst()
peripheralDevice?.writeValue(nextCmd, for: characteristic, type: .withoutResponse)
}
// 上报到 Dart 层
notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap)
return return
} }
// 计算CRC校验 - 不包括最后一个字节(CRC) // 计算CRC校验 - 不包括最后一个字节(CRC)
let payload = data.prefix(data.count - 1) let payload = data.prefix(data.count - 1)
let calculatedCrc = calculateCRC8(data: payload) let calculatedCrc = calculateCRC8(data: payload)
let receivedCrc = data[data.count - 1] let receivedCrc = data[data.count - 1]
if calculatedCrc != receivedCrc { if calculatedCrc != receivedCrc {
os_log("设备响应CRC校验失败: 计算=0x%02X, 接收=0x%02X", log: logger, type: .error, calculatedCrc, receivedCrc) os_log("设备响应CRC校验失败: 计算=0x%02X, 接收=0x%02X", log: logger, type: .error, calculatedCrc, receivedCrc)
return return
} }
// 提取响应数据 // 提取响应数据
let responseData = length > 0 ? data.subdata(in: 3..<(3 + Int(length))) : Data() let responseData = length > 0 ? data.subdata(in: 3..<(3 + Int(length))) : Data()
isReply = true; isReply = true
cmdReplyType = cmd; cmdReplyType = cmd
if !commandQueue.isEmpty { if !commandQueue.isEmpty {
let nextCmd = commandQueue.removeFirst() let nextCmd = commandQueue.removeFirst()
peripheralDevice?.writeValue(nextCmd, for: characteristic, type: .withoutResponse) peripheralDevice?.writeValue(nextCmd, for: characteristic, type: .withoutResponse)
} }
// 根据命令类型处理设备响应 // 根据命令类型处理设备响应
switch cmd { switch cmd {
@ -928,103 +982,98 @@ private var cmdReplyType: UInt8 = 0
"command": "versionInfo", "command": "versionInfo",
"rawData": responseData "rawData": responseData
] ]
if !responseData.isEmpty { if !responseData.isEmpty {
// 如果无法解析为字符串,以点分隔的数字表示版本
let versionNums = responseData let versionNums = responseData
.map { $0 } .map { $0 }
.filter { $0 != 0xFF } // 过滤掉无效字节 .filter { $0 != 0xFF }
.map { String($0) } .map { String($0) }
.joined(separator: ".") .joined(separator: ".")
resultMap["version"] = versionNums resultMap["version"] = versionNums
os_log("接收到目标版本号(数字格式): %@", log: logger, type: .info, versionNums) os_log("接收到目标版本号(数字格式): %@", log: logger, type: .info, versionNums)
} }
notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap)
case BleConst.CMD_GET_PRODUCT_ID: case BleConst.CMD_GET_PRODUCT_ID:
var resultMap: [String: Any] = [ var resultMap: [String: Any] = [
"success": true, "success": true,
"command": "productId", "command": "productId",
"rawData": responseData "rawData": responseData
] ]
if !responseData.isEmpty { if !responseData.isEmpty {
let productId = responseData.map { String($0 & 0xFF) }.joined(separator: ".") let productId = responseData.map { String($0 & 0xFF) }.joined(separator: ".")
resultMap["productId"] = productId resultMap["productId"] = productId
os_log("接收到产品ID: %d", log: logger, type: .info, productId) os_log("接收到产品ID: %d", log: logger, type: .info, productId)
} }
notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap)
case BleConst.CMD_GET_COLOR_ID: case BleConst.CMD_GET_COLOR_ID:
var resultMap: [String: Any] = [ var resultMap: [String: Any] = [
"success": true, "success": true,
"command": "colorInfo", "command": "colorInfo",
"rawData": responseData "rawData": responseData
] ]
if !responseData.isEmpty { if !responseData.isEmpty {
let colorId = Int(responseData[0]) let colorId = Int(responseData[0])
resultMap["colorId"] = colorId resultMap["colorId"] = colorId
os_log("接收到耳机颜色ID: %d", log: logger, type: .info, colorId) os_log("接收到耳机颜色ID: %d", log: logger, type: .info, colorId)
} }
notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap)
case BleConst.CMD_GET_BATTERY_INFO: case BleConst.CMD_GET_BATTERY_INFO:
var resultMap: [String: Any] = [ var resultMap: [String: Any] = [
"success": true, "success": true,
"command": "batteryInfo", "command": "batteryInfo",
"rawData": responseData "rawData": responseData
] ]
if responseData.count >= 3 { if responseData.count >= 3 {
// 处理电池信息 - 左耳、右耳、充电仓
let leftEarBattery = Int(responseData[0] & 0x7F) let leftEarBattery = Int(responseData[0] & 0x7F)
let leftEarCharging = (responseData[0] & 0x80) != 0 let leftEarCharging = (responseData[0] & 0x80) != 0
let rightEarBattery = Int(responseData[1] & 0x7F) let rightEarBattery = Int(responseData[1] & 0x7F)
let rightEarCharging = (responseData[1] & 0x80) != 0 let rightEarCharging = (responseData[1] & 0x80) != 0
let caseBattery = Int(responseData[2] & 0x7F) let caseBattery = Int(responseData[2] & 0x7F)
let caseCharging = (responseData[2] & 0x80) != 0 let caseCharging = (responseData[2] & 0x80) != 0
// 添加到结果Map
resultMap["leftEarBattery"] = leftEarBattery resultMap["leftEarBattery"] = leftEarBattery
resultMap["leftEarCharging"] = leftEarCharging resultMap["leftEarCharging"] = leftEarCharging
resultMap["rightEarBattery"] = rightEarBattery resultMap["rightEarBattery"] = rightEarBattery
resultMap["rightEarCharging"] = rightEarCharging resultMap["rightEarCharging"] = rightEarCharging
resultMap["caseBattery"] = caseBattery resultMap["caseBattery"] = caseBattery
resultMap["caseCharging"] = caseCharging resultMap["caseCharging"] = caseCharging
resultMap["batteryLevel"] = leftEarBattery // 整体电量用左耳表示 resultMap["batteryLevel"] = leftEarBattery
os_log("接收到电量信息: 左耳=%d%% (%@), 右耳=%d%% (%@), 充电仓=%d%% (%@)", os_log("接收到电量信息: 左耳=%d%% (%@), 右耳=%d%% (%@), 充电仓=%d%% (%@)",
log: logger, type: .info, log: logger, type: .info,
leftEarBattery, leftEarCharging ? "充电中" : "未充电", leftEarBattery, leftEarCharging ? "充电中" : "未充电",
rightEarBattery, rightEarCharging ? "充电中" : "未充电", rightEarBattery, rightEarCharging ? "充电中" : "未充电",
caseBattery, caseCharging ? "充电中" : "未充电") caseBattery, caseCharging ? "充电中" : "未充电")
} else if !responseData.isEmpty { } else if !responseData.isEmpty {
// 兼容单字节电量
let batteryLevel = Int(responseData[0]) let batteryLevel = Int(responseData[0])
resultMap["batteryLevel"] = batteryLevel resultMap["batteryLevel"] = batteryLevel
os_log("接收到电量信息(简化): %d%%", log: logger, type: .info, batteryLevel) os_log("接收到电量信息(简化): %d%%", log: logger, type: .info, batteryLevel)
} }
notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap)
case BleConst.CMD_CONTROL_CODEC: case BleConst.CMD_CONTROL_CODEC:
var resultMap: [String: Any] = [ var resultMap: [String: Any] = [
"success": true, "success": true,
"command": Int(cmd), "command": Int(cmd),
"rawData": responseData "rawData": responseData
] ]
if responseData.count >= 2 { if !responseData.isEmpty && responseData.count >= 2 {
let codecStatus = Int(responseData[0]) let codecStatus = Int(responseData[0])
let channelMode = Int(responseData[1]) let channelMode = Int(responseData[1])
// 解码状态描述
let statusDesc: String let statusDesc: String
switch codecStatus { switch codecStatus {
case Int(BleConst.CODEC_CONTROL_CLOSE): case Int(BleConst.CODEC_CONTROL_CLOSE):
@ -1040,8 +1089,7 @@ private var cmdReplyType: UInt8 = 0
default: default:
statusDesc = "未知状态(\(codecStatus))" statusDesc = "未知状态(\(codecStatus))"
} }
// 声道模式描述
let channelDesc: String let channelDesc: String
switch channelMode { switch channelMode {
case Int(BleConst.AUDIO_CHANNEL_LEFT): case Int(BleConst.AUDIO_CHANNEL_LEFT):
@ -1053,19 +1101,29 @@ private var cmdReplyType: UInt8 = 0
default: default:
channelDesc = "未知声道(\(channelMode))" channelDesc = "未知声道(\(channelMode))"
} }
resultMap["codecStatus"] = codecStatus resultMap["codecStatus"] = codecStatus
resultMap["channelMode"] = channelMode resultMap["channelMode"] = channelMode
resultMap["statusDesc"] = statusDesc resultMap["statusDesc"] = statusDesc
resultMap["channelDesc"] = channelDesc resultMap["channelDesc"] = channelDesc
os_log("编解码控制响应: %@, %@", log: logger, type: .info, statusDesc, channelDesc) os_log("编解码控制响应: %@, %@", log: logger, type: .info, statusDesc, channelDesc)
} else if !responseData.isEmpty && responseData.count == 1 {
// 与 Android 保持一致:单字节且为 0 判定为错误
let codecStatus = Int(responseData[0])
if codecStatus == 0x00 {
os_log("收到设备响应: 失败, codecStatus: %d", log: logger, type: .debug, codecStatus)
resultMap = [
"success": true, // 与 Android 对齐(Dart 层按 rawData 判断)
"command": "Error",
"rawData": responseData
]
}
} }
notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap) notifyDeviceInfoReceived(infoType: Int(cmd), infoData: resultMap)
default: default:
// 对于未知命令,返回原始数据
let resultMap: [String: Any] = [ let resultMap: [String: Any] = [
"success": true, "success": true,
"command": Int(cmd), "command": Int(cmd),
@ -1151,11 +1209,12 @@ private var cmdReplyType: UInt8 = 0
case BleConst.CMD_WAKE_UP: case BleConst.CMD_WAKE_UP:
os_log("📢 收到唤醒指令", log: logger, type: .info) os_log("📢 收到唤醒指令", log: logger, type: .info)
os_log("🔍 当前状态检查: 连接状态=%d, writeCharacteristic=%@, notifyCharacteristic=%@, audioCharacteristic=%@", os_log("🔍 当前状态检查: 连接状态=%d, writeCharacteristic=%@, notifyCharacteristic=%@, callAudioCharacteristic=%@, audioCharacteristic=%@",
log: logger, type: .debug, log: logger, type: .debug,
connectionState, connectionState,
writeCharacteristic != nil ? "✅" : "❌", writeCharacteristic != nil ? "✅" : "❌",
notifyCharacteristic != nil ? "✅" : "❌", notifyCharacteristic != nil ? "✅" : "❌",
callAudioCharacteristic != nil ? "✅" : "❌",
audioCharacteristic != nil ? "✅" : "❌") audioCharacteristic != nil ? "✅" : "❌")
@ -1250,7 +1309,7 @@ extension BleService: SwiftOpusAudioProcessor.AudioDataCallback {
os_log("🎵 接收到编码音频数据: %d bytes", log: logger, type: .info, data.count) os_log("🎵 接收到编码音频数据: %d bytes", log: logger, type: .info, data.count)
// 修复条件绑定语句 // 修复条件绑定语句
guard let characteristic = callAudioCharacteristic, guard let characteristic = callWriteCharacteristic,
let peripheral = peripheralDevice, let peripheral = peripheralDevice,
connectionState == BleConst.STATE_CONNECTED else { connectionState == BleConst.STATE_CONNECTED else {
os_log("通话音频特征未设置或设备未连接", log: logger, type: .error) os_log("通话音频特征未设置或设备未连接", log: logger, type: .error)
@ -1358,7 +1417,7 @@ extension BleService: SwiftOpusAudioProcessor.AudioDataCallback {
* @param data 要发送的音频数据 * @param data 要发送的音频数据
*/ */
private func sendAudioChunk(_ data: Data) { private func sendAudioChunk(_ data: Data) {
guard let characteristic = callAudioCharacteristic, guard let characteristic = callWriteCharacteristic,
let peripheral = peripheralDevice, let peripheral = peripheralDevice,
connectionState == BleConst.STATE_CONNECTED else { connectionState == BleConst.STATE_CONNECTED else {
os_log("通话音频特征未设置或设备未连接,无法发送音频数据", log: logger, type: .error) os_log("通话音频特征未设置或设备未连接,无法发送音频数据", log: logger, type: .error)
@ -1764,7 +1823,7 @@ extension BleService: CBPeripheralDelegate {
}else if service.uuid == BleConst.CALL_AUDIO_SERVICE_UUID { }else if service.uuid == BleConst.CALL_AUDIO_SERVICE_UUID {
os_log("🔍 发现通话音频服务,开始发现通话音频特征...", log: logger, type: .debug) os_log("🔍 发现通话音频服务,开始发现通话音频特征...", log: logger, type: .debug)
// 通话音频服务,发现通话音频特征 // 通话音频服务,发现通话音频特征
peripheral.discoverCharacteristics([BleConst.CALL_WRITE_AUDIO_CHAR_UUID], for: service) peripheral.discoverCharacteristics([BleConst.CALL_WRITE_AUDIO_CHAR_UUID, BleConst.CALL_RECEIVE_AUDIO_CHAR_UUID], for: service)
} }
} }
} }
@ -1796,15 +1855,21 @@ extension BleService: CBPeripheralDelegate {
os_log("🎵 音频特征已设置,开始发现描述符...", log: logger, type: .info) os_log("🎵 音频特征已设置,开始发现描述符...", log: logger, type: .info)
// 先发现描述符,再配置通知 // 先发现描述符,再配置通知
peripheral.discoverDescriptors(for: characteristic) peripheral.discoverDescriptors(for: characteristic)
}else if characteristic.uuid == BleConst.CALL_WRITE_AUDIO_CHAR_UUID { }else if characteristic.uuid == BleConst.CALL_RECEIVE_AUDIO_CHAR_UUID {
callAudioCharacteristic = characteristic callAudioCharacteristic = characteristic
os_log("🎵 通话音频特征已设置,开始发现描述符...", log: logger, type: .info) os_log("🎵 通话音频特征已设置,开始发现描述符...", log: logger, type: .info)
// 先发现描述符,再配置通知 // 先发现描述符,再配置通知
peripheral.discoverDescriptors(for: characteristic)
}
else if characteristic.uuid == BleConst.CALL_WRITE_AUDIO_CHAR_UUID {
callWriteCharacteristic = characteristic
os_log("🎵 通话音频特征已设置,开始发现描述符...", log: logger, type: .info)
// 先发现描述符,再配置通知
} }
} }
// 检查所有必要特征是否已准备就绪 // 检查所有必要特征是否已准备就绪
let isReady = writeCharacteristic != nil && notifyCharacteristic != nil && audioCharacteristic != nil let isReady = writeCharacteristic != nil && notifyCharacteristic != nil && audioCharacteristic != nil && callAudioCharacteristic != nil
if isReady { if isReady {
os_log("🚀 所有BLE特征已准备就绪,可以发送命令", log: logger, type: .info) os_log("🚀 所有BLE特征已准备就绪,可以发送命令", log: logger, type: .info)
} else { } else {
@ -1812,6 +1877,7 @@ extension BleService: CBPeripheralDelegate {
log: logger, type: .debug, log: logger, type: .debug,
writeCharacteristic != nil ? "✅" : "❌", writeCharacteristic != nil ? "✅" : "❌",
notifyCharacteristic != nil ? "✅" : "❌", notifyCharacteristic != nil ? "✅" : "❌",
callAudioCharacteristic != nil ? "✅" : "❌",
audioCharacteristic != nil ? "✅" : "❌") audioCharacteristic != nil ? "✅" : "❌")
} }
} }
@ -1829,6 +1895,9 @@ extension BleService: CBPeripheralDelegate {
} else if characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID { } else if characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID {
// 处理音频数据 // 处理音频数据
processAudioData(data) processAudioData(data)
}else if characteristic.uuid == BleConst.CALL_RECEIVE_AUDIO_CHAR_UUID {
// 处理音频数据
processAudioData(data)
} }
} }
@ -1850,7 +1919,7 @@ extension BleService: CBPeripheralDelegate {
characteristic.uuid.uuidString, characteristic.descriptors?.count ?? 0) characteristic.uuid.uuidString, characteristic.descriptors?.count ?? 0)
// 对通知特征和音频特征配置通知 // 对通知特征和音频特征配置通知
if characteristic.uuid == BleConst.NOTIFY_CHAR_UUID || characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID { if characteristic.uuid == BleConst.NOTIFY_CHAR_UUID || characteristic.uuid == BleConst.RECEIVE_AUDIO_CHAR_UUID || characteristic.uuid == BleConst.CALL_RECEIVE_AUDIO_CHAR_UUID {
setupNotification(for: peripheral, characteristic: characteristic) setupNotification(for: peripheral, characteristic: characteristic)
} }
} }

33
local_plugins/native_plugin/.gitignore

@ -0,0 +1,33 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
build/

33
local_plugins/native_plugin/.metadata

@ -0,0 +1,33 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "35c388afb57ef061d06a39b537336c87e0e3d1b1"
channel: "stable"
project_type: plugin
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: android
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: ios
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

3
local_plugins/native_plugin/CHANGELOG.md

@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

1
local_plugins/native_plugin/LICENSE

@ -0,0 +1 @@
TODO: Add your license here.

15
local_plugins/native_plugin/README.md

@ -0,0 +1,15 @@
# native_plugin
A new Flutter plugin project.
## Getting Started
This project is a starting point for a Flutter
[plug-in package](https://flutter.dev/to/develop-plugins),
a specialized package that includes platform-specific implementation code for
Android and/or iOS.
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

4
local_plugins/native_plugin/analysis_options.yaml

@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

9
local_plugins/native_plugin/android/.gitignore

@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.cxx

52
local_plugins/native_plugin/android/build.gradle

@ -0,0 +1,52 @@
group = "com.example.native_plugin"
version = "1.0"
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.7.0")
}
}
rootProject.allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: "com.android.library"
android {
namespace = "com.example.native_plugin"
compileSdk = 35
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
defaultConfig {
minSdk = 21
}
dependencies {
testImplementation("junit:junit:4.13.2")
testImplementation("org.mockito:mockito-core:5.0.0")
}
testOptions {
unitTests.all {
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen {false}
showStandardStreams = true
}
}
}
}

1
local_plugins/native_plugin/android/settings.gradle

@ -0,0 +1 @@
rootProject.name = 'native_plugin'

3
local_plugins/native_plugin/android/src/main/AndroidManifest.xml

@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.native_plugin">
</manifest>

38
local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/NativePlugin.java

@ -0,0 +1,38 @@
package com.example.native_plugin;
import androidx.annotation.NonNull;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
/** NativePlugin */
public class NativePlugin implements FlutterPlugin, MethodCallHandler {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private MethodChannel channel;
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "native_plugin");
channel.setMethodCallHandler(this);
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
if (call.method.equals("getPlatformVersion")) {
result.success("Android " + android.os.Build.VERSION.RELEASE);
} else {
result.notImplemented();
}
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
channel.setMethodCallHandler(null);
}
}

29
local_plugins/native_plugin/android/src/test/java/com/example/native_plugin/NativePluginTest.java

@ -0,0 +1,29 @@
package com.example.native_plugin;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import org.junit.Test;
/**
* This demonstrates a simple unit test of the Java portion of this plugin's implementation.
*
* Once you have built the plugin's example app, you can run these tests from the command
* line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or
* you can run them directly from IDEs that support JUnit such as Android Studio.
*/
public class NativePluginTest {
@Test
public void onMethodCall_getPlatformVersion_returnsExpectedValue() {
NativePlugin plugin = new NativePlugin();
final MethodCall call = new MethodCall("getPlatformVersion", null);
MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
plugin.onMethodCall(call, mockResult);
verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE);
}
}

38
local_plugins/native_plugin/ios/.gitignore

@ -0,0 +1,38 @@
.idea/
.vagrant/
.sconsign.dblite
.svn/
.DS_Store
*.swp
profile
DerivedData/
build/
GeneratedPluginRegistrant.h
GeneratedPluginRegistrant.m
.generated/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
xcuserdata
*.moved-aside
*.pyc
*sync/
Icon?
.tags*
/Flutter/Generated.xcconfig
/Flutter/ephemeral/
/Flutter/flutter_export_environment.sh

0
local_plugins/native_plugin/ios/Assets/.gitkeep

15
local_plugins/native_plugin/ios/Classes/FLNativeView.h

@ -0,0 +1,15 @@
#import <flutter/flutter.h>
@interface FLNativeViewFactory : NSObject <FlutterPlatformViewFactory>
- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger> *)messenger;
@end
@interface FLNativeView : NSObject <FlutterBinaryMessenger>
- (instancetype)initWithFrame:(CGRect)frame
viewIdentifier:(int64_t)viewId
arguments:(id _Nullable)args
binaryMessenger:(NSObject<FlutterBinaryMessenger> *)messenger;
- (UIView *)view;
@end

83
local_plugins/native_plugin/ios/Classes/FLNativeView.m

@ -0,0 +1,83 @@
#import "FLNativeView.h"
#import "PlayerView.h"
@implementation FLNativeViewFactory {
NSObject<FlutterBinaryMessenger> *_messenger;
}
- (instancetype)initWithMessenger:
(NSObject<FlutterBinaryMessenger> *)messenger {
self = [super init];
if (self) {
_messenger = messenger;
}
return self;
}
- (NSObject<FlutterPlatformView> *)createWithFrame:(CGRect)frame
viewIdentifier:(int64_t)viewId
arguments:(id _Nullable)args {
return [[FLNativeView alloc] initWithFrame:frame
viewIdentifier:viewId
arguments:args
binaryMessenger:_messenger];
}
/// Implementing this method is only necessary when the `arguments` in
/// `createWithFrame` is not `nil`.
- (NSObject<FlutterMessageCodec> *)createArgsCodec {
return [FlutterStandardMessageCodec sharedInstance];
}
@end
@implementation FLNativeView {
PlayerView *_view;
FlutterMethodChannel *_methodChannel;
}
- (instancetype)initWithFrame:(CGRect)frame
viewIdentifier:(int64_t)viewId
arguments:(id _Nullable)args
binaryMessenger:(NSObject<FlutterBinaryMessenger> *)messenger {
if (self = [super init]) {
_view = [[PlayerView alloc] init];
_methodChannel = [FlutterMethodChannel
methodChannelWithName:
[NSString
stringWithFormat:@"native_plugin/native_view_%lld", viewId]
binaryMessenger:messenger];
__weak typeof(self) weakSelf = self;
[_methodChannel setMethodCallHandler:^(FlutterMethodCall *_Nonnull call,
FlutterResult _Nonnull result) {
typeof(self) strongSelf = weakSelf;
if (strongSelf != nil) {
[strongSelf onMethodCall:call result:result];
}
}];
}
return self;
}
- (UIView *)view {
return _view;
}
- (void)onMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
if ([@"getInternalView" isEqualToString:call.method]) {
result(@((uint64_t)(_view)));
} else if ([@"updateContent" isEqualToString:call.method]) {
NSDictionary *data = call.arguments;
NSString *lang = data[@"lang"];
NSString *cn = data[@"cn"];
NSString *en = data[@"en"];
dispatch_async(dispatch_get_main_queue(), ^{
[_view updateWithLang:lang cn:cn en:en];
});
result(nil);
} else {
result(@(0));
}
}
@end

4
local_plugins/native_plugin/ios/Classes/NativePlugin.h

@ -0,0 +1,4 @@
#import <Flutter/Flutter.h>
@interface NativePlugin : NSObject<FlutterPlugin>
@end

114
local_plugins/native_plugin/ios/Classes/NativePlugin.m

@ -0,0 +1,114 @@
#import "NativePlugin.h"
#import "FLNativeView.h"
#import "PlayerView.h"
#import <AVKit/AVKit.h>
@interface NativePlugin ()
@property(nonatomic, strong) NSMutableArray *_Nonnull pipContentViewArray;
@end
@implementation NativePlugin
+ (void)updateAudioSession {
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
NSError *categoryError = nil;
if (@available(iOS 14.5, *)) {
[audioSession
setCategory:AVAudioSessionCategoryPlayback
mode:AVAudioSessionModeMoviePlayback
options:
AVAudioSessionCategoryOptionOverrideMutedMicrophoneInterruption
error:&categoryError];
} else {
// Fallback on earlier versions
}
if (categoryError) {
NSLog(@"Set audio session category error: %@",
categoryError.localizedDescription);
}
NSError *activeError = nil;
[audioSession setActive:YES error:&activeError];
if (activeError) {
NSLog(@"Set audio session active error: %@",
activeError.localizedDescription);
}
}
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
// must call this, otherwise pip will not possible
// https://github.com/jazzychad/PiPBugDemo
[NativePlugin updateAudioSession];
FlutterMethodChannel *channel =
[FlutterMethodChannel methodChannelWithName:@"native_plugin"
binaryMessenger:[registrar messenger]];
NativePlugin *instance = [[NativePlugin alloc] init];
FLNativeViewFactory *factory =
[[FLNativeViewFactory alloc] initWithMessenger:registrar.messenger];
[registrar registerViewFactory:factory withId:@"native_view"];
[registrar addMethodCallDelegate:instance channel:channel];
}
- (instancetype)init {
self = [super init];
if (self) {
_pipContentViewArray = [NSMutableArray array];
}
return self;
}
- (void)handleMethodCall:(FlutterMethodCall *)call
result:(FlutterResult)result {
if ([@"getPlatformVersion" isEqualToString:call.method]) {
result([@"iOS "
stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
} else if ([@"createPipContentView" isEqualToString:call.method]) {
PlayerView *playerView = [[PlayerView alloc] init];
// should control the play state by event of pip, optimize it later
[playerView play];
[self.pipContentViewArray addObject:playerView];
result(@((uint64_t)playerView));
} else if ([@"disposePipContentView" isEqualToString:call.method]) {
uint64_t viewId = [call.arguments unsignedLongLongValue];
for (PlayerView *playerView in self.pipContentViewArray) {
if ((uint64_t)playerView == viewId) {
[playerView pause];
[self.pipContentViewArray removeObject:playerView];
break;
}
}
} else if ([@"updatePipContentView" isEqualToString:call.method]) {
NSDictionary *data = call.arguments;
NSString *lang = data[@"lang"];
NSString *cn = data[@"cn"];
NSString *en = data[@"en"];
NSNumber *viewIdNum = data[@"viewId"];
dispatch_async(dispatch_get_main_queue(), ^{
if (viewIdNum != nil) {
uint64_t targetId = viewIdNum.unsignedLongLongValue;
for (PlayerView *playerView in self.pipContentViewArray) {
if ((uint64_t)playerView == targetId) {
[playerView updateWithLang:lang cn:cn en:en];
break;
}
}
} else {
PlayerView *playerView = [self.pipContentViewArray lastObject];
[playerView updateWithLang:lang cn:cn en:en];
}
});
result(nil);
} else {
result(FlutterMethodNotImplemented);
}
}
@end

15
local_plugins/native_plugin/ios/Classes/PlayerView.h

@ -0,0 +1,15 @@
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface PlayerView : UIView
- (void)play;
- (void)pause;
- (void)updateWithLang:(NSString *)lang cn:(NSString *)cn en:(NSString *)en;
@end
NS_ASSUME_NONNULL_END

100
local_plugins/native_plugin/ios/Classes/PlayerView.m

@ -0,0 +1,100 @@
#import "PlayerView.h"
@interface PlayerView ()
@property(nonatomic, strong) UILabel *titleLabel;
@property(nonatomic, strong) UILabel *langLabel;
@property(nonatomic, strong) UILabel *cnLabel;
@property(nonatomic, strong) UILabel *enLabel;
@property(nonatomic, strong) UIStackView *stackView;
@end
@implementation PlayerView
- (instancetype)init {
if (self = [super init]) {
self.backgroundColor = [UIColor whiteColor];
UILabel *title = [[UILabel alloc] init];
title.text = @"实时翻译";
title.font = [UIFont boldSystemFontOfSize:22];
title.textColor = [UIColor blackColor];
title.textAlignment = NSTextAlignmentCenter;
title.numberOfLines = 1;
self.titleLabel = title;
UILabel *lang = [[UILabel alloc] init];
lang.text = @"中文(简体) -> 英语";
lang.font = [UIFont systemFontOfSize:14];
lang.textColor = [UIColor darkGrayColor];
lang.textAlignment = NSTextAlignmentLeft;
lang.numberOfLines = 1;
self.langLabel = lang;
UILabel *cn = [[UILabel alloc] init];
cn.font = [UIFont systemFontOfSize:18];
cn.textColor = [UIColor blackColor];
cn.textAlignment = NSTextAlignmentLeft;
cn.numberOfLines = 0;
self.cnLabel = cn;
UILabel *en = [[UILabel alloc] init];
en.font = [UIFont systemFontOfSize:18];
en.textColor = [UIColor colorWithRed:0.24 green:0.54 blue:0.90 alpha:1.0];
en.textAlignment = NSTextAlignmentLeft;
en.numberOfLines = 0;
self.enLabel = en;
NSMutableParagraphStyle *para = [[NSMutableParagraphStyle alloc] init];
para.lineSpacing = 4;
self.cnLabel.attributedText = [[NSAttributedString alloc] initWithString:@"正在识别..." attributes:@{NSParagraphStyleAttributeName: para}];
self.enLabel.attributedText = [[NSAttributedString alloc] initWithString:@"翻译中..." attributes:@{NSParagraphStyleAttributeName: para}];
UIStackView *stack = [[UIStackView alloc] initWithArrangedSubviews:@[self.titleLabel, self.langLabel, self.cnLabel, self.enLabel]];
stack.axis = UILayoutConstraintAxisVertical;
stack.spacing = 12;
stack.alignment = UIStackViewAlignmentFill;
stack.distribution = UIStackViewDistributionFill;
stack.translatesAutoresizingMaskIntoConstraints = NO;
self.stackView = stack;
self.layoutMargins = UIEdgeInsetsMake(16, 16, 16, 16);
[self addSubview:self.stackView];
UILayoutGuide *guide = self.layoutMarginsGuide;
[NSLayoutConstraint activateConstraints:@[
[self.stackView.topAnchor constraintEqualToAnchor:guide.topAnchor],
[self.stackView.leadingAnchor constraintEqualToAnchor:guide.leadingAnchor],
[self.stackView.trailingAnchor constraintEqualToAnchor:guide.trailingAnchor],
[self.stackView.bottomAnchor constraintLessThanOrEqualToAnchor:guide.bottomAnchor]
]];
}
return self;
}
- (void)play {
}
- (void)pause {
}
- (void)updateWithLang:(NSString *)lang cn:(NSString *)cn en:(NSString *)en {
if (lang != nil) {
self.langLabel.text = lang;
}
NSMutableParagraphStyle *para = [[NSMutableParagraphStyle alloc] init];
para.lineSpacing = 4;
if (cn != nil) {
self.cnLabel.attributedText = [[NSAttributedString alloc] initWithString:cn attributes:@{NSParagraphStyleAttributeName: para}];
}
if (en != nil) {
self.enLabel.attributedText = [[NSAttributedString alloc] initWithString:en attributes:@{NSParagraphStyleAttributeName: para}];
}
}
@end

14
local_plugins/native_plugin/ios/Resources/PrivacyInfo.xcprivacy

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
</dict>
</plist>

29
local_plugins/native_plugin/ios/native_plugin.podspec

@ -0,0 +1,29 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint native_plugin.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'native_plugin'
s.version = '0.0.1'
s.summary = 'A new Flutter plugin project.'
s.description = <<-DESC
A new Flutter plugin project.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.public_header_files = 'Classes/**/*.h'
s.dependency 'Flutter'
s.platform = :ios, '12.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
# If your plugin requires a privacy manifest, for example if it uses any
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
# plugin's privacy impact, and then uncomment this line. For more information,
# see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
# s.resource_bundles = {'native_plugin_privacy' => ['Resources/PrivacyInfo.xcprivacy']}
end

19
local_plugins/native_plugin/lib/native_plugin.dart

@ -0,0 +1,19 @@
import 'native_plugin_platform_interface.dart';
class NativePlugin {
Future<String?> getPlatformVersion() {
return NativePluginPlatform.instance.getPlatformVersion();
}
Future<int> createPipContentView() {
return NativePluginPlatform.instance.createPipContentView();
}
Future<void> updatePipContentView(Map data) {
return NativePluginPlatform.instance.updatePipContentView(data);
}
Future<void> disposePipContentView(int viewId) {
return NativePluginPlatform.instance.disposePipContentView(viewId);
}
}

37
local_plugins/native_plugin/lib/native_plugin_method_channel.dart

@ -0,0 +1,37 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'native_plugin_platform_interface.dart';
/// An implementation of [NativePluginPlatform] that uses method channels.
class MethodChannelNativePlugin extends NativePluginPlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('native_plugin');
@override
Future<String?> getPlatformVersion() async {
final version = await methodChannel.invokeMethod<String>(
'getPlatformVersion',
);
return version;
}
@override
Future<int> createPipContentView() async {
final viewId = await methodChannel.invokeMethod<int>(
'createPipContentView',
);
return viewId ?? 0;
}
@override
Future<void> updatePipContentView(Map data) async {
await methodChannel.invokeMethod<void>('updatePipContentView', data);
}
@override
Future<void> disposePipContentView(int viewId) async {
await methodChannel.invokeMethod<void>('disposePipContentView', viewId);
}
}

47
local_plugins/native_plugin/lib/native_plugin_platform_interface.dart

@ -0,0 +1,47 @@
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'native_plugin_method_channel.dart';
abstract class NativePluginPlatform extends PlatformInterface {
/// Constructs a NativePluginPlatform.
NativePluginPlatform() : super(token: _token);
static final Object _token = Object();
static NativePluginPlatform _instance = MethodChannelNativePlugin();
/// The default instance of [NativePluginPlatform] to use.
///
/// Defaults to [MethodChannelNativePlugin].
static NativePluginPlatform get instance => _instance;
/// Platform-specific implementations should set this with their own
/// platform-specific class that extends [NativePluginPlatform] when
/// they register themselves.
static set instance(NativePluginPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
Future<String?> getPlatformVersion() {
throw UnimplementedError('platformVersion() has not been implemented.');
}
Future<int> createPipContentView() {
throw UnimplementedError(
'createPipContentView() has not been implemented.',
);
}
Future<void> updatePipContentView(Map data) {
throw UnimplementedError(
'updatePipContentView() has not been implemented.',
);
}
Future<void> disposePipContentView(int viewId) {
throw UnimplementedError(
'disposePipContentView() has not been implemented.',
);
}
}

72
local_plugins/native_plugin/pubspec.yaml

@ -0,0 +1,72 @@
name: native_plugin
description: "A new Flutter plugin project."
version: 0.0.1
homepage:
environment:
sdk: ^3.7.0
flutter: '>=3.3.0'
dependencies:
flutter:
sdk: flutter
plugin_platform_interface: ^2.0.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# This section identifies this Flutter project as a plugin project.
# The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
# which should be registered in the plugin registry. This is required for
# using method channels.
# The Android 'package' specifies package in which the registered class is.
# This is required for using method channels on Android.
# The 'ffiPlugin' specifies that native code should be built and bundled.
# This is required for using `dart:ffi`.
# All these are used by the tooling to maintain consistency when
# adding or updating assets for this project.
plugin:
platforms:
android:
package: com.example.native_plugin
pluginClass: NativePlugin
ios:
pluginClass: NativePlugin
# To add assets to your plugin package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/to/asset-from-package
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# To add custom fonts to your plugin package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/to/font-from-package

10
pubspec.yaml

@ -1,7 +1,7 @@
name: voitrans name: voitrans
description: "Voitrans - AI Voice Assistant." description: "Voitrans - AI Voice Assistant."
publish_to: "none" publish_to: "none"
version: 1.0.20+62 version: 1.0.19+61
environment: environment:
sdk: ">=3.3.0 <4.0.0" sdk: ">=3.3.0 <4.0.0"
@ -80,6 +80,7 @@ dependencies:
dio: ^5.8.0+1 dio: ^5.8.0+1
sqflite: ^2.4.2 sqflite: ^2.4.2
path: ^1.9.1 path: ^1.9.1
pip: ^0.0.3
connectivity_plus: ^6.1.5 # 网络连接检测依赖 connectivity_plus: ^6.1.5 # 网络连接检测依赖
speech: speech:
path: local_plugins/speech path: local_plugins/speech
@ -115,8 +116,11 @@ dependencies:
path: local_plugins/user_portrait path: local_plugins/user_portrait
qq_music: qq_music:
path: local_plugins/qq_music path: local_plugins/qq_music
native_plugin:
path: local_plugins/native_plugin
test: test:
path: local_plugins/test path: local_plugins/test
in_app_purchase: ^3.2.3 # 应用内购买插件
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -142,8 +146,8 @@ flutter:
- assets/fonts/NotoSansSC-Regular.ttf - assets/fonts/NotoSansSC-Regular.ttf
- assets/lottie/ - assets/lottie/
- assets/js/ - assets/js/
- assets/images/headphone_new4.png - assets/images/defaultPicture.png
- assets/images/headphone_new3.png - assets/images/defaultPicture.jpg
- assets/images/qqmusic.png - assets/images/qqmusic.png
- assets/images/music.png - assets/images/music.png
- assets/weather/ - assets/weather/

Loading…
Cancel
Save