Browse Source

优化翻译代码结构

weicu
fdp 1 year ago
parent
commit
7d0ba4daef
  1. 8
      lib/modules/translation/bindings/translation_history_binding.dart
  2. 93
      lib/modules/translation/controllers/record_timer_controller.dart
  3. 179
      lib/modules/translation/controllers/translation_audiohistory_controller.dart
  4. 1085
      lib/modules/translation/controllers/translation_controller.dart
  5. 82
      lib/modules/translation/controllers/translation_detail_controller.dart
  6. 503
      lib/modules/translation/controllers/translation_history_controller.dart
  7. 76
      lib/modules/translation/models/translation_models.dart
  8. 3
      lib/modules/translation/views/translation_detail_view.dart
  9. 6
      lib/modules/translation/views/translation_history_view.dart
  10. 1
      lib/modules/translation/views/translation_view.dart
  11. 305
      local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift
  12. 93
      local_plugins/ble_service/ios/Classes/BleConst.swift
  13. 1266
      local_plugins/ble_service/ios/Classes/BleService.swift
  14. 4
      local_plugins/ble_service/ios/Classes/BleServicePlugin.h
  15. 12
      local_plugins/ble_service/ios/Classes/BleServicePlugin.m
  16. 374
      local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift
  17. 239
      local_plugins/ble_service/ios/Classes/SwiftOpusAudioProcessor.swift
  18. 1
      local_plugins/ble_service/ios/Classes/XCFrameworkExample.swift

8
lib/modules/translation/bindings/translation_history_binding.dart

@ -1,11 +1,11 @@
import 'package:get/get.dart';
import '../controllers/translation_history_controller.dart';
import '../controllers/translation_audiohistory_controller.dart';
class TranslationHistoryBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<TranslationHistoryController>(
() => TranslationHistoryController(),
Get.lazyPut<TranslationAudioHistoryController>(
() => TranslationAudioHistoryController(),
);
}
}
}

93
lib/modules/translation/controllers/record_timer_controller.dart

@ -0,0 +1,93 @@
import 'dart:async';
import 'package:get/get.dart';
import '../../../core/utils/logger.dart';
/// 录音计时器管理类
/// 负责管理录音过程中的计时功能
class RecordTimerController extends GetxController {
static const String TAG = 'RecordTimerController';
// 计时器相关状态
final RxString recordDurationText = '00:00'.obs;
Timer? _recordTimer;
int _seconds = 0;
bool _isTimerPaused = false;
/// 开始计时器
/// 从00:00开始计时,每秒更新一次显示
void startTimer() {
_seconds = 0;
recordDurationText.value = '00:00';
_recordTimer = Timer.periodic(Duration(seconds: 1), (_) {
_seconds++;
final minutes = (_seconds ~/ 60).toString().padLeft(2, '0');
final seconds = (_seconds % 60).toString().padLeft(2, '0');
recordDurationText.value = '$minutes:$seconds';
});
Logger.info('录音计时器已开始');
}
/// 暂停计时器
/// 保持当前时间显示,停止计时
void pauseTimer() {
if (_recordTimer != null && !_isTimerPaused) {
_recordTimer?.cancel();
_recordTimer = null;
_isTimerPaused = true;
Logger.info('录音计时器已暂停');
}
}
/// 恢复计时器
/// 从暂停的时间点继续计时
void resumeTimer() {
if (_isTimerPaused) {
_recordTimer = Timer.periodic(Duration(seconds: 1), (_) {
_seconds++;
final minutes = (_seconds ~/ 60).toString().padLeft(2, '0');
final seconds = (_seconds % 60).toString().padLeft(2, '0');
recordDurationText.value = '$minutes:$seconds';
});
_isTimerPaused = false;
Logger.info('录音计时器已恢复');
}
}
/// 重置计时器
/// 停止计时并重置为00:00
void resetTimer() {
_recordTimer?.cancel();
_recordTimer = null;
_seconds = 0;
_isTimerPaused = false;
recordDurationText.value = '00:00';
Logger.info('录音计时器已重置');
}
/// 停止计时器
/// 完全停止计时器并重置状态
void stopTimer() {
_recordTimer?.cancel();
_recordTimer = null;
_isTimerPaused = false;
recordDurationText.value = '00:00';
_seconds = 0;
Logger.info('录音计时器已停止');
}
/// 获取当前计时秒数
int get currentSeconds => _seconds;
/// 获取当前是否暂停状态
bool get isPaused => _isTimerPaused;
/// 获取当前是否正在计时
bool get isRunning => _recordTimer != null && !_isTimerPaused;
/// 释放资源
void dispose() {
_recordTimer?.cancel();
_recordTimer = null;
Logger.info('录音计时器管理器已释放');
}
}

179
lib/modules/translation/controllers/translation_audiohistory_controller.dart

@ -0,0 +1,179 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import '../controllers/translation_controller.dart';
import 'dart:async';
import '../models/translation_models.dart'; // 添加异步支持
class TranslationAudioHistoryController extends GetxController {
// 引用主翻译控制器,以访问历史记录数据
final TranslationController translationController =
Get.find<TranslationController>();
// 按日期分组的历史记录
final Rx<Map<String, List<TranslationItem>>> groupedHistory =
Rx<Map<String, List<TranslationItem>>>({});
// 排序后的日期列表
final RxList<String> sortedDates = <String>[].obs;
// 会话映射表,用于存储每个会话的所有项目
final Rx<Map<String, List<TranslationItem>>> sessionMap =
Rx<Map<String, List<TranslationItem>>>({});
// 加载状态
final isLoading = true.obs;
@override
void onInit() {
super.onInit();
// 确保所有翻译操作都已停止
translationController.stopAll();
// 异步加载数据,避免阻塞主线程
loadHistoryData();
}
// 异步加载历史数据
Future<void> loadHistoryData() async {
isLoading.value = true;
// 使用 compute 或 Future.microtask 在后台处理数据
await Future.microtask(() {
_groupHistoryByDate();
_groupHistoryBySession();
});
isLoading.value = false;
}
// 按日期对历史记录进行分组
void _groupHistoryByDate() {
final Map<String, List<TranslationItem>> newGroupedHistory = {};
// 获取完整的历史记录,而不仅仅是当前显示的记录
final List<TranslationItem> allHistory =
translationController.getAllHistory();
// 筛选出每个会话的第一条记录
final List<TranslationItem> firstItemsOfSessions =
allHistory.where((item) => item.isFirstInSession).toList();
for (var item in firstItemsOfSessions) {
final DateTime date = item.timestamp;
String key;
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final itemDate = DateTime(date.year, date.month, date.day);
if (itemDate == today) {
key = 'TODAY';
} else {
key = DateFormat('yyyy-MM-dd').format(date);
}
if (!newGroupedHistory.containsKey(key)) {
newGroupedHistory[key] = [];
}
newGroupedHistory[key]!.add(item);
}
// 对每个日期组内的项目按时间倒序排序
newGroupedHistory.forEach((key, items) {
items.sort((a, b) => b.timestamp.compareTo(a.timestamp));
});
// 更新分组后的历史记录
groupedHistory.value = newGroupedHistory;
// 按日期排序
final List<String> dates = newGroupedHistory.keys.toList()
..sort((a, b) {
if (a == 'TODAY') return -1;
if (b == 'TODAY') return 1;
if (a == '1 DAY AGO') return -1;
if (b == '1 DAY AGO') return 1;
if (a == '2 DAYS AGO') return -1;
if (b == '2 DAYS AGO') return 1;
return b.compareTo(a); // 降序排列其他日期
});
sortedDates.value = dates;
}
// 按会话ID对历史记录进行分组
void _groupHistoryBySession() {
final Map<String, List<TranslationItem>> newSessionMap = {};
// 获取完整的历史记录进行会话分组
final List<TranslationItem> allHistory =
translationController.getAllHistory();
for (var item in allHistory) {
if (!newSessionMap.containsKey(item.sessionId)) {
newSessionMap[item.sessionId] = [];
}
newSessionMap[item.sessionId]!.add(item);
}
// 对每个会话内的项目按时间升序排序
newSessionMap.forEach((key, items) {
items.sort((a, b) => a.timestamp.compareTo(b.timestamp));
});
// 更新会话映射表
sessionMap.value = newSessionMap;
}
// 获取指定会话的所有翻译项
List<TranslationItem> getSessionItems(String sessionId) {
return sessionMap.value[sessionId] ?? [];
}
// 播放翻译项
void playTranslationItem(TranslationItem item) {
translationController.playTranslationItem(item);
}
// 播放指定文本
void playText(String text) {
translationController.playTranslatedText(text);
}
// 检查是否有历史记录
bool get hasHistory => translationController.translationHistory.isNotEmpty;
// 删除指定会话的历史记录
Future<void> deleteSessionHistory(String sessionId) async {
try {
// 调用主控制器的删除方法
translationController.deleteSessionHistory(sessionId);
// 重新加载和分组历史数据
await loadHistoryData();
// 显示删除成功提示
Get.snackbar(
'删除成功',
'已删除该会话的所有翻译记录',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200],
colorText: Get.isDarkMode ? Colors.white : Colors.black,
duration: const Duration(seconds: 2),
);
} catch (e) {
// 显示删除失败提示
Get.snackbar(
'删除失败',
'删除会话记录时发生错误: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[400],
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
}
}
}

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

File diff suppressed because it is too large

82
lib/modules/translation/controllers/translation_detail_controller.dart

@ -2,7 +2,8 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../data/services/volcano_ai_service.dart';
import '../controllers/translation_controller.dart';
import '../controllers/translation_history_controller.dart';
import 'translation_audiohistory_controller.dart';
import '../models/translation_models.dart'; // 添加这行导入
import 'package:get_storage/get_storage.dart';
import 'dart:async';
import '../../../core/utils/logger.dart';
@ -13,25 +14,25 @@ class TranslationDetailController extends GetxController {
final VolcanoAIService _aiService = Get.find<VolcanoAIService>();
final GetStorage _storage = GetStorage();
late final TranslationUsageService _usageService;
// 存储键前缀
static const String _summaryKeyPrefix = 'translation_summary_';
// 超时设置(秒)
static const int _defaultTimeoutDuration = 30;
static const int _extendedTimeoutDuration = 60;
// 状态变量
final selectedTabIndex = 0.obs;
final isGeneratingSummary = false.obs;
final summaryResult = ''.obs;
final isLoading = true.obs;
// 当前会话信息
late final TranslationItem currentItem;
late final List<TranslationItem> sessionItems;
late final TranslationHistoryController historyController;
late final TranslationAudioHistoryController historyController;
@override
void onInit() {
super.onInit();
@ -59,7 +60,7 @@ class TranslationDetailController extends GetxController {
isLoading.value = false;
}
}
// 异步初始化方法
Future<void> _asyncInitialize() async {
isLoading.value = true;
@ -76,7 +77,7 @@ class TranslationDetailController extends GetxController {
isLoading.value = false;
}
}
// 加载保存的摘要
void loadSavedSummary() {
try {
@ -88,62 +89,64 @@ class TranslationDetailController extends GetxController {
Logger.error('加载摘要失败: ${e.toString()}');
}
}
// 保存摘要
void saveSummary(String summary) {
if (summary.isEmpty) return;
try {
_storage.write(_getSummaryKey(), summary);
} catch (e) {
Logger.error('保存摘要失败: ${e.toString()}');
}
}
// 获取摘要的存储键
String _getSummaryKey() => _summaryKeyPrefix + currentItem.sessionId;
// 切换选项卡
void switchTab(int index) => selectedTabIndex.value = index;
// 播放翻译项
void playTranslationItem(TranslationItem item) => historyController.playTranslationItem(item);
void playTranslationItem(TranslationItem item) =>
historyController.playTranslationItem(item);
// 播放文本
void playText(String text) => historyController.playText(text);
// 获取合并的源文本
String getCombinedSourceText() => sessionItems.map((e) => e.sourceText).join('\n\n');
String getCombinedSourceText() =>
sessionItems.map((e) => e.sourceText).join('\n\n');
// 获取合并的翻译文本
String getCombinedTranslatedText() => sessionItems.map((e) => e.translatedText).join('\n\n');
String getCombinedTranslatedText() =>
sessionItems.map((e) => e.translatedText).join('\n\n');
// 使用延长的超时时间重试提取摘要
Future<String> retryExtractSummaryWithExtendedTimeout(String text) async {
return _extractSummaryInternal(text, _extendedTimeoutDuration);
}
// 提取摘要
Future<String> extractSummary(String text) async {
return _extractSummaryInternal(text, _defaultTimeoutDuration);
}
// 内部摘要提取方法,避免代码重复
Future<String> _extractSummaryInternal(String text, int timeoutDuration) async {
Future<String> _extractSummaryInternal(
String text, int timeoutDuration) async {
if (text.isEmpty) {
return '无法生成摘要:文本为空';
}
isGeneratingSummary.value = true;
try {
// 构建提示词
final messages = [
{'role': 'user', 'content': '请为以下文本生成一个简洁的摘要,突出重点内容:\n\n$text'},
];
// 系统提示词
const systemPrompt = '''
你是一个专业的文本摘要助手,擅长从会议记录、对话和文本中提取关键信息并生成简洁的摘要。
@ -160,24 +163,26 @@ class TranslationDetailController extends GetxController {
4. 摘要总长度应该是原文的1/3或更短
5. 确保列表中的每个要点都简明扼要,突出重点
''';
String result;
try {
// 使用超时限制
result = await _aiService.sendMessage(
result = await _aiService
.sendMessage(
messages: messages,
systemPrompt: systemPrompt,
).timeout(Duration(seconds: timeoutDuration), onTimeout: () {
)
.timeout(Duration(seconds: timeoutDuration), onTimeout: () {
throw TimeoutException('请求超时', Duration(seconds: timeoutDuration));
});
} on TimeoutException catch (_) {
final timeoutMessage = timeoutDuration == _defaultTimeoutDuration
? '生成摘要超时,请稍后重试'
final timeoutMessage = timeoutDuration == _defaultTimeoutDuration
? '生成摘要超时,请稍后重试'
: '生成摘要超时,请检查网络连接后重试';
summaryResult.value = timeoutMessage;
return summaryResult.value;
}
// 更新结果并保存
if (result.isNotEmpty) {
summaryResult.value = result;
@ -201,20 +206,19 @@ class TranslationDetailController extends GetxController {
isGeneratingSummary.value = false;
}
}
// 删除单条翻译记录
Future<void> deleteTranslationItem(TranslationItem item) async {
try {
// 调用主控制器的删除方法
historyController.translationController.deleteTranslationItem(
item.sessionId,
item.timestamp
);
historyController.translationController
.deleteTranslationItem(item.sessionId, item.timestamp);
// 从当前会话项目列表中移除该项目
sessionItems.removeWhere((sessionItem) =>
sessionItem.sessionId == item.sessionId &&
sessionItem.timestamp.millisecondsSinceEpoch == item.timestamp.millisecondsSinceEpoch);
sessionItem.timestamp.millisecondsSinceEpoch ==
item.timestamp.millisecondsSinceEpoch);
// 重新加载历史控制器的数据
await historyController.loadHistoryData();
@ -261,4 +265,4 @@ class TranslationDetailController extends GetxController {
historyController.translationController.stopAll();
super.onClose();
}
}
}

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

@ -1,174 +1,371 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import '../controllers/translation_controller.dart';
import 'dart:async'; // 添加异步支持
class TranslationHistoryController extends GetxController {
// 引用主翻译控制器,以访问历史记录数据
final TranslationController translationController = Get.find<TranslationController>();
// 按日期分组的历史记录
final Rx<Map<String, List<TranslationItem>>> groupedHistory = Rx<Map<String, List<TranslationItem>>>({});
// 排序后的日期列表
final RxList<String> sortedDates = <String>[].obs;
// 会话映射表,用于存储每个会话的所有项目
final Rx<Map<String, List<TranslationItem>>> sessionMap = Rx<Map<String, List<TranslationItem>>>({});
// 加载状态
final isLoading = true.obs;
@override
void onInit() {
super.onInit();
// 确保所有翻译操作都已停止
translationController.stopAll();
// 异步加载数据,避免阻塞主线程
loadHistoryData();
import 'package:get_storage/get_storage.dart';
import '../../../core/utils/logger.dart';
import '../models/translation_models.dart';
/// 翻译历史记录管理类
/// 负责管理翻译历史的存储、加载、分页等功能
class TranslationHistoryManager extends GetxController {
static const String TAG = 'TranslationHistoryManager';
// 存储相关配置
static const String _historyKey = 'translation_history';
static const int _maxStorageItems = 1000; // 存储限制
static const int _maxDisplayItems = 20; // 显示限制
final GetStorage _storage = GetStorage();
// 翻译历史数据
final translationHistory = <TranslationItem>[].obs;
// 分页加载相关状态
final isLoadingMore = false.obs;
final hasMoreHistory = true.obs;
final currentPage = 1.obs;
final pageSize = 20;
final isLoadingMoreError = false.obs;
bool _scrollListenerSetup = false;
// 滚动控制器
final ScrollController scrollController = ScrollController();
/// 初始化历史管理器
void initialize() {
loadTranslationHistory();
_setupScrollListener();
}
// 异步加载历史数据
Future<void> loadHistoryData() async {
isLoading.value = true;
// 使用 compute 或 Future.microtask 在后台处理数据
await Future.microtask(() {
_groupHistoryByDate();
_groupHistoryBySession();
});
isLoading.value = false;
/// 加载翻译历史记录
/// 初始化时只加载最近的显示条数,提高性能
void loadTranslationHistory() {
final historyJson = _storage.read(_historyKey);
if (historyJson != null) {
try {
final List<dynamic> historyList = jsonDecode(historyJson);
final List<TranslationItem> allHistory =
historyList.map((item) => TranslationItem.fromJson(item)).toList();
// 初始化分页状态
currentPage.value = 1;
isLoadingMore.value = false;
isLoadingMoreError.value = false;
// 主页面只显示最近的记录,保证界面性能
if (allHistory.length > _maxDisplayItems) {
translationHistory.value =
allHistory.sublist(allHistory.length - _maxDisplayItems);
hasMoreHistory.value = true;
} else {
translationHistory.value = allHistory;
hasMoreHistory.value = false;
}
Logger.info('翻译历史加载成功: ${translationHistory.length}条记录');
} catch (e) {
_storage.remove(_historyKey);
Logger.error('加载翻译历史失败: ${e.toString()}');
}
} else {
// 没有历史数据时初始化分页状态
hasMoreHistory.value = false;
currentPage.value = 1;
isLoadingMore.value = false;
isLoadingMoreError.value = false;
}
}
// 按日期对历史记录进行分组
void _groupHistoryByDate() {
final Map<String, List<TranslationItem>> newGroupedHistory = {};
// 获取完整的历史记录,而不仅仅是当前显示的记录
final List<TranslationItem> allHistory = translationController.getAllHistory();
// 筛选出每个会话的第一条记录
final List<TranslationItem> firstItemsOfSessions = allHistory
.where((item) => item.isFirstInSession)
.toList();
for (var item in firstItemsOfSessions) {
final DateTime date = item.timestamp;
String key;
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final itemDate = DateTime(date.year, date.month, date.day);
if (itemDate == today) {
key = 'TODAY';
}
else {
key = DateFormat('yyyy-MM-dd').format(date);
/// 保存翻译历史记录
/// 只保存完成态记录,过滤中间态记录
void saveTranslationHistory() {
try {
// 只保存完成态记录
final List<TranslationItem> finalRecords =
translationHistory.where((item) => !item.isIntermediate).toList();
// 应用存储限制
List<TranslationItem> historyToSave = finalRecords;
if (finalRecords.length > _maxStorageItems) {
historyToSave =
finalRecords.sublist(finalRecords.length - _maxStorageItems);
}
if (!newGroupedHistory.containsKey(key)) {
newGroupedHistory[key] = [];
// 保存到本地存储
final List<Map<String, dynamic>> historyJson =
historyToSave.map((item) => item.toJson()).toList();
_storage.write(_historyKey, jsonEncode(historyJson));
Logger.info('翻译历史保存成功: 存储${historyToSave.length}条记录');
} catch (e) {
Logger.error('保存翻译历史失败: ${e.toString()}');
}
}
/// 添加新的翻译记录
/// [item] 要添加的翻译记录
/// [scrollToBottom] 是否滚动到底部
void addTranslationItem(TranslationItem item, {bool scrollToBottom = true}) {
translationHistory.add(item);
if (scrollToBottom) {
_scrollToBottom();
}
}
/// 更新翻译记录
/// [timestamp] 记录的时间戳,用于定位记录
/// [updater] 更新函数
bool updateTranslationItem(
DateTime timestamp, Function(TranslationItem) updater) {
final index = translationHistory
.indexWhere((item) => item.timestamp.isAtSameMomentAs(timestamp));
if (index != -1) {
updater(translationHistory[index]);
translationHistory.refresh();
return true;
}
return false;
}
/// 加载更多历史记录(分页加载)
Future<void> loadMoreHistory() async {
if (isLoadingMore.value || !hasMoreHistory.value) {
return;
}
try {
isLoadingMore.value = true;
isLoadingMoreError.value = false;
final nextPage = currentPage.value + 1;
Logger.i(TAG, '正在加载第 $nextPage 页翻译历史...');
// 从存储中获取所有历史记录
final historyJson = _storage.read(_historyKey);
if (historyJson != null) {
final List<dynamic> historyList = jsonDecode(historyJson);
final List<TranslationItem> allHistory =
historyList.map((item) => TranslationItem.fromJson(item)).toList();
// 计算分页参数
final totalItems = allHistory.length;
final endIndex = totalItems - ((nextPage - 1) * pageSize);
final startIndex = totalItems - (nextPage * pageSize);
if (endIndex > 0) {
final actualStartIndex = startIndex < 0 ? 0 : startIndex;
final actualEndIndex = endIndex > totalItems ? totalItems : endIndex;
final pageItems =
allHistory.sublist(actualStartIndex, actualEndIndex);
if (pageItems.isNotEmpty) {
await Future.delayed(const Duration(milliseconds: 300));
// reverse模式下:将新记录插入到现有记录列表的开头
final updatedHistory = [...pageItems, ...translationHistory];
translationHistory.assignAll(updatedHistory);
// 更新分页状态
currentPage.value = nextPage;
hasMoreHistory.value = startIndex > 0;
Logger.i(TAG, '成功加载 ${pageItems.length} 条历史记录');
} else {
hasMoreHistory.value = false;
}
} else {
hasMoreHistory.value = false;
}
} else {
hasMoreHistory.value = false;
}
newGroupedHistory[key]!.add(item);
} catch (e) {
Logger.e(TAG, '加载更多历史记录失败: $e');
isLoadingMoreError.value = true;
} finally {
isLoadingMore.value = false;
}
// 对每个日期组内的项目按时间倒序排序
newGroupedHistory.forEach((key, items) {
items.sort((a, b) => b.timestamp.compareTo(a.timestamp));
});
// 更新分组后的历史记录
groupedHistory.value = newGroupedHistory;
// 按日期排序
final List<String> dates = newGroupedHistory.keys.toList()
..sort((a, b) {
if (a == 'TODAY') return -1;
if (b == 'TODAY') return 1;
if (a == '1 DAY AGO') return -1;
if (b == '1 DAY AGO') return 1;
if (a == '2 DAYS AGO') return -1;
if (b == '2 DAYS AGO') return 1;
return b.compareTo(a); // 降序排列其他日期
});
sortedDates.value = dates;
}
// 按会话ID对历史记录进行分组
void _groupHistoryBySession() {
final Map<String, List<TranslationItem>> newSessionMap = {};
// 获取完整的历史记录进行会话分组
final List<TranslationItem> allHistory = translationController.getAllHistory();
/// 获取所有完成态历史记录
List<TranslationItem> getAllHistory() {
try {
final List<TranslationItem> result = <TranslationItem>[];
final Set<String> existingIds = <String>{};
// 添加存储的历史记录
final historyJson = _storage.read(_historyKey);
if (historyJson != null) {
final List<dynamic> historyList = jsonDecode(historyJson);
final List<TranslationItem> storedHistory =
historyList.map((item) => TranslationItem.fromJson(item)).toList();
for (var item in allHistory) {
if (!newSessionMap.containsKey(item.sessionId)) {
newSessionMap[item.sessionId] = [];
for (final item in storedHistory) {
final id =
'${item.sessionId}_${item.timestamp.millisecondsSinceEpoch}';
if (!existingIds.contains(id)) {
existingIds.add(id);
result.add(item);
}
}
}
newSessionMap[item.sessionId]!.add(item);
// 添加当前内存中的完成态记录
for (final item in translationHistory) {
if (!item.isIntermediate) {
final id =
'${item.sessionId}_${item.timestamp.millisecondsSinceEpoch}';
if (!existingIds.contains(id)) {
existingIds.add(id);
result.add(item);
}
}
}
// 按时间排序
result.sort((a, b) => a.timestamp.compareTo(b.timestamp));
return result;
} catch (e) {
Logger.error('获取完整历史记录失败: ${e.toString()}');
return translationHistory.where((item) => !item.isIntermediate).toList();
}
// 对每个会话内的项目按时间升序排序
newSessionMap.forEach((key, items) {
items.sort((a, b) => a.timestamp.compareTo(b.timestamp));
});
// 更新会话映射表
sessionMap.value = newSessionMap;
}
// 获取指定会话的所有翻译项
List<TranslationItem> getSessionItems(String sessionId) {
return sessionMap.value[sessionId] ?? [];
}
// 播放翻译项
void playTranslationItem(TranslationItem item) {
translationController.playTranslationItem(item);
/// 清除所有翻译历史
void clearTranslationHistory() {
translationHistory.clear();
_storage.remove(_historyKey);
Logger.info('翻译历史已清除');
}
// 播放指定文本
void playText(String text) {
translationController.playTranslatedText(text);
/// 删除指定会话的所有翻译记录
void deleteSessionHistory(String sessionId) {
try {
// 从内存中删除
final originalCount = translationHistory.length;
translationHistory.removeWhere((item) => item.sessionId == sessionId);
final removedFromMemory = originalCount - translationHistory.length;
// 从存储中删除
final historyJson = _storage.read(_historyKey);
int removedFromStorage = 0;
if (historyJson != null) {
final List<dynamic> historyList = jsonDecode(historyJson);
final List<TranslationItem> storedHistory =
historyList.map((item) => TranslationItem.fromJson(item)).toList();
final originalStoredCount = storedHistory.length;
storedHistory.removeWhere((item) => item.sessionId == sessionId);
removedFromStorage = originalStoredCount - storedHistory.length;
// 重新保存
final List<Map<String, dynamic>> updatedHistoryJson =
storedHistory.map((item) => item.toJson()).toList();
_storage.write(_historyKey, jsonEncode(updatedHistoryJson));
}
translationHistory.refresh();
Logger.info(
'删除会话历史成功: sessionId=$sessionId, 内存删除${removedFromMemory}条, 存储删除${removedFromStorage}条');
} catch (e) {
Logger.error('删除会话历史失败: ${e.toString()}');
rethrow;
}
}
// 检查是否有历史记录
bool get hasHistory => translationController.translationHistory.isNotEmpty;
// 删除指定会话的历史记录
Future<void> deleteSessionHistory(String sessionId) async {
/// 删除指定的单条翻译记录
void deleteTranslationItem(String sessionId, DateTime timestamp) {
try {
// 调用主控制器的删除方法
translationController.deleteSessionHistory(sessionId);
// 重新加载和分组历史数据
await loadHistoryData();
// 显示删除成功提示
Get.snackbar(
'删除成功',
'已删除该会话的所有翻译记录',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200],
colorText: Get.isDarkMode ? Colors.white : Colors.black,
duration: const Duration(seconds: 2),
);
// 从内存中删除
final originalCount = translationHistory.length;
translationHistory.removeWhere((item) =>
item.sessionId == sessionId &&
item.timestamp.millisecondsSinceEpoch ==
timestamp.millisecondsSinceEpoch);
final removedFromMemory = originalCount - translationHistory.length;
// 从存储中删除
final historyJson = _storage.read(_historyKey);
int removedFromStorage = 0;
if (historyJson != null) {
final List<dynamic> historyList = jsonDecode(historyJson);
final List<TranslationItem> storedHistory =
historyList.map((item) => TranslationItem.fromJson(item)).toList();
final originalStoredCount = storedHistory.length;
storedHistory.removeWhere((item) =>
item.sessionId == sessionId &&
item.timestamp.millisecondsSinceEpoch ==
timestamp.millisecondsSinceEpoch);
removedFromStorage = originalStoredCount - storedHistory.length;
// 重新保存
final List<Map<String, dynamic>> updatedHistoryJson =
storedHistory.map((item) => item.toJson()).toList();
_storage.write(_historyKey, jsonEncode(updatedHistoryJson));
}
translationHistory.refresh();
Logger.info(
'删除单条翻译记录成功: sessionId=$sessionId, 内存删除${removedFromMemory}条, 存储删除${removedFromStorage}条');
} catch (e) {
// 显示删除失败提示
Get.snackbar(
'删除失败',
'删除会话记录时发生错误: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red[400],
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
Logger.error('删除单条翻译记录失败: ${e.toString()}');
rethrow;
}
}
}
/// 滚动到底部
void scrollToBottom({bool animate = true}) {
_scrollToBottom(animate: animate);
}
/// 设置滚动监听器
void _setupScrollListener() {
if (_scrollListenerSetup) return;
scrollController.addListener(() {
// reverse模式下:检查是否滚动到底部
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 100) {
if (hasMoreHistory.value && !isLoadingMore.value) {
Logger.d(TAG, '触发加载更多翻译历史记录');
loadMoreHistory();
}
}
});
_scrollListenerSetup = true;
}
/// 内部滚动到底部方法
void _scrollToBottom({bool animate = true}) {
if (scrollController.hasClients && translationHistory.isNotEmpty) {
try {
const targetPosition = 0.0; // reverse模式下最新消息在顶部
if (animate) {
scrollController.animateTo(
targetPosition,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
} else {
scrollController.jumpTo(targetPosition);
}
} catch (e) {
Logger.error('滚动到顶部失败: ${e.toString()}');
}
}
}
/// 释放资源
void dispose() {
scrollController.dispose();
Logger.info('翻译历史管理器已释放');
}
}

76
lib/modules/translation/models/translation_models.dart

@ -0,0 +1,76 @@
import 'package:get/get.dart';
import '../../../data/services/language_manager.dart';
/// 翻译项模型
/// 表示一条翻译记录的完整信息
class TranslationItem {
String sourceText;
String translatedText;
String sourceLanguageCode;
String targetLanguageCode;
final DateTime timestamp;
String sessionId;
final bool isFirstInSession;
bool isIntermediate; // 是否为中间结果
TranslationItem({
required this.sourceText,
required this.translatedText,
required this.sourceLanguageCode,
required this.targetLanguageCode,
required this.timestamp,
required this.sessionId,
this.isFirstInSession = false,
this.isIntermediate = false,
});
/// 从JSON创建TranslationItem实例
factory TranslationItem.fromJson(Map<String, dynamic> json) {
DateTime timestamp;
try {
timestamp = DateTime.parse(json['timestamp']);
} catch (e) {
timestamp = DateTime.now();
}
return TranslationItem(
sourceText: json['sourceText'] ?? '',
translatedText: json['translatedText'] ?? '',
sourceLanguageCode: json['sourceLanguageCode'] ?? 'zh-CN',
targetLanguageCode: json['targetLanguageCode'] ?? 'en-US',
timestamp: timestamp,
sessionId:
json['sessionId'] ?? DateTime.now().millisecondsSinceEpoch.toString(),
isFirstInSession: json['isFirstInSession'] ?? false,
isIntermediate: false, // 从存储加载的都是最终结果
);
}
/// 转换为JSON格式
Map<String, dynamic> toJson() {
return {
'sourceText': sourceText,
'translatedText': translatedText,
'sourceLanguageCode': sourceLanguageCode,
'targetLanguageCode': targetLanguageCode,
'timestamp': timestamp.toIso8601String(),
'sessionId': sessionId,
'isFirstInSession': isFirstInSession,
// 不保存isIntermediate,因为存储的都是最终结果
};
}
/// 获取源语言的中文名称
String get sourceLanguageName {
final languageManager = Get.find<LanguageManager>();
return languageManager.getChineseNameByAsrCode(sourceLanguageCode) ??
'未知语言';
}
/// 获取目标语言的中文名称
String get targetLanguageName {
final languageManager = Get.find<LanguageManager>();
return languageManager.getChineseNameByAsrCode(targetLanguageCode) ??
'未知语言';
}
}

3
lib/modules/translation/views/translation_detail_view.dart

@ -2,12 +2,13 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/translation_controller.dart';
import '../controllers/translation_history_controller.dart';
import '../controllers/translation_audiohistory_controller.dart';
import '../controllers/translation_detail_controller.dart';
import 'package:intl/intl.dart';
import 'package:flutter/services.dart';
import 'package:share_plus/share_plus.dart';
import '../../../core/theme/app_colors.dart';
import '../models/translation_models.dart';
class TranslationDetailView extends GetView<TranslationDetailController> {
TranslationDetailView({Key? key}) : super(key: key);

6
lib/modules/translation/views/translation_history_view.dart

@ -3,13 +3,15 @@ import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:intl/intl.dart';
import '../controllers/translation_controller.dart';
import '../controllers/translation_history_controller.dart';
import '../controllers/translation_audiohistory_controller.dart';
import '../bindings/translation_detail_binding.dart';
import '../models/translation_models.dart';
import 'translation_detail_view.dart';
import '../../../core/theme/app_colors.dart';
import '../../../data/services/language_manager.dart';
class TranslationHistoryView extends GetView<TranslationHistoryController> {
class TranslationHistoryView
extends GetView<TranslationAudioHistoryController> {
const TranslationHistoryView({Key? key}) : super(key: key);
@override

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

@ -4,6 +4,7 @@ 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 '../views/translation_history_view.dart';
import '../bindings/translation_history_binding.dart';
import '../../../core/theme/app_colors.dart';

305
local_plugins/ble_service/ios/Classes/BleAccessorySetup.swift

@ -1,305 +0,0 @@
import Foundation
import CoreBluetooth
import os.log
import AVFoundation
import AccessorySetupKit
import UIKit
class BleAccessorySetup: NSObject {
// MARK: - 属性
static let shared = BleAccessorySetup()
private var bleService: BleService?
private var session: ASAccessorySession?
private var isSessionActivated = false
private var isInitializing = false
private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleAccessorySetup")
// 杰理蓝牙设备的公司标识符
private let deviceCompanyIdentifier: UInt16 = 0xEFAB
private let deviceDisplayName = "语音助理耳机"
// 用于存储配对设备UUID的UserDefaults键
private let pairedDeviceUUIDKey = "com.yunqiinnovation.ble_service.paired_device_uuid"
typealias DeviceDiscoveredCallback = (UUID) -> Void
private var onDeviceDiscovered: DeviceDiscoveredCallback?
// MARK: - 初始化
private override init() { super.init() }
// MARK: - 公共方法
func setBleService(_ service: Any) {
self.bleService = service as? BleService
}
func setDeviceDiscoveredCallback(_ callback: @escaping (UUID) -> Void) {
self.onDeviceDiscovered = callback
}
// 处理发现的设备并连接
func connectToAccessory(uuid: UUID) {
guard let service = bleService else {
return
}
service.connectToAccessory(uuid: uuid)
}
func addToReconnectQueue(uuid: UUID) {
guard let service = bleService else {
return
}
service.addToReconnectQueue(uuid: uuid)
}
// 从视图控制器显示设备选择器 - 主要入口点
func showDevicePickerFromViewController(_ viewController: UIViewController) {
initializeSession()
}
// 清除蓝牙状态(仅用于测试或重置,不会清除系统授权)
func resetBluetoothState() {
if let service = bleService {
service.disconnect()
}
}
// MARK: - 会话管理
func initializeSession() {
if isInitializing {
os_log("会话正在初始化中,跳过", log: logger, type: .info)
return
}
// 如果会话已激活,检查是否有配对设备
if session != nil && isSessionActivated {
os_log("会话已激活,检查配对设备", log: logger, type: .info)
// 会话已激活,检查是否有配对设备
if let accessories = session?.accessories, !accessories.isEmpty {
os_log("发现 %d 个已配对设备,尝试连接", log: logger, type: .info, accessories.count)
// 有配对设备,尝试连接
if let service = bleService {
let _ = service.getCentralManager()
for accessory in accessories {
if let uuid = accessory.bluetoothIdentifier {
onDeviceDiscovered?(uuid)
connectToAccessory(uuid: uuid)
}
}
}
} else {
os_log("没有已配对设备,显示配对框", log: logger, type: .info)
// 没有配对设备,直接显示配对框
showDevicePicker()
}
return
}
os_log("开始初始化新会话", log: logger, type: .info)
isInitializing = true
do {
session = try ASAccessorySession()
session?.activate(on: .main) { [weak self] event in
guard let self = self else { return }
self.handleSessionEvent(event)
}
} catch {
os_log("会话初始化失败: %{public}@", log: logger, type: .error, error.localizedDescription)
handleSessionInitError(error)
}
}
func resetSession() {
session?.invalidate()
session = nil
isSessionActivated = false
isInitializing = false
}
// MARK: - 设备选择器显示
private func showDevicePicker() {
guard let session = session, isSessionActivated else {
os_log("无法显示配对框:会话未激活", log: logger, type: .error)
return
}
os_log("准备显示设备配对框", log: logger, type: .info)
let descriptor = ASDiscoveryDescriptor()
descriptor.bluetoothCompanyIdentifier = ASBluetoothCompanyIdentifier(deviceCompanyIdentifier)
let deviceImage = UIImage(systemName: "headphones") ?? UIImage()
let displayItem = ASPickerDisplayItem(
name: deviceDisplayName,
productImage: deviceImage,
descriptor: descriptor
)
DispatchQueue.main.async {
os_log("正在显示设备配对框", log: self.logger, type: .info)
session.showPicker(for: [displayItem]) { [weak self] error in
guard let self = self else { return }
if let error = error {
let nsError = error as NSError
os_log("设备选择器错误: %{public}@", log: self.logger, type: .error, error.localizedDescription)
} else {
os_log("设备选择器完成", log: self.logger, type: .info)
}
}
}
}
// MARK: - 私有辅助方法
private func handleSessionEvent(_ event: ASAccessoryEvent) {
switch event.eventType {
case .activated:
os_log("会话已激活", log: logger, type: .info)
handleSessionActivated()
case .accessoryAdded:
os_log("发现新设备: %{public}@", log: logger, type: .info, event.accessory?.bluetoothIdentifier?.uuidString ?? "Unknown")
handleAccessoryAdded(event.accessory)
case .accessoryRemoved:
os_log("设备已移除: %{public}@", log: logger, type: .info, event.accessory?.bluetoothIdentifier?.uuidString ?? "Unknown")
handleAccessoryRemoved(event.accessory)
case .accessoryChanged:
os_log("设备状态已改变", log: logger, type: .info)
break
case .pickerDidPresent:
os_log("配对框已显示", log: logger, type: .info)
break
case .pickerDidDismiss:
os_log("配对框已关闭", log: logger, type: .info)
break
case .pickerSetupFailed:
os_log("选择器设置失败: %{public}@", log: logger, type: .error,
event.error?.localizedDescription ?? "未知错误")
default:
os_log("未处理的会话事件: %d", log: logger, type: .info, event.eventType.rawValue)
break
}
}
private func handleSessionActivated() {
isSessionActivated = true
isInitializing = false
if let accessories = session?.accessories, !accessories.isEmpty {
if let service = bleService {
let _ = service.getCentralManager()
for accessory in accessories {
if let uuid = accessory.bluetoothIdentifier {
onDeviceDiscovered?(uuid)
connectToAccessory(uuid: uuid)
}
}
}
} else {
showDevicePicker()
}
}
private func handleAccessoryAdded(_ accessory: ASAccessory?) {
guard let accessory = accessory, let uuid = accessory.bluetoothIdentifier else {
return
}
// 保存配对设备的UUID
savePairedMacAddress(uuid)
onDeviceDiscovered?(uuid)
connectToAccessory(uuid: uuid)
}
private func handleAccessoryRemoved(_ accessory: ASAccessory?) {
guard let accessory = accessory, let uuid = accessory.bluetoothIdentifier else {
return
}
if let service = bleService {
service.disconnect()
}
}
private func handleSessionInitError(_ error: Error) {
isInitializing = false
session = nil
}
// MARK: - 蓝牙状态检查
func isBluetoothAvailable() -> Bool {
do {
let audioSession = AVAudioSession.sharedInstance()
let outputs = audioSession.currentRoute.outputs
for output in outputs {
if output.portType == .bluetoothA2DP ||
output.portType == .bluetoothHFP ||
output.portType == .bluetoothLE {
return true
}
}
} catch {
os_log("检查蓝牙状态出错: %{public}@", log: logger, type: .error, error.localizedDescription)
}
return true
}
// MARK: - 清除配对关系
func clearAssociations() {
resetBluetoothState()
// 清除保存的配对设备UUID
clearPairedMacAddress()
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
resetSession()
initializeSession()
}
#endif
}
// MARK: - 配对设备UUID管理
/// 获取已配对设备的UUID字符串
/// 返回存储的配对设备UUID,如果没有则返回nil
func getPairedMacAddress() -> String? {
let uuidString = UserDefaults.standard.string(forKey: pairedDeviceUUIDKey)
if let uuidString = uuidString, !uuidString.isEmpty {
os_log("获取已配对设备UUID: %{public}@", log: logger, type: .info, uuidString)
return uuidString
}
os_log("未找到已配对设备UUID", log: logger, type: .info)
return nil
}
/// 保存配对设备的UUID
/// - Parameter uuid: 要保存的设备UUID
func savePairedMacAddress(_ uuid: UUID) {
let uuidString = uuid.uuidString
UserDefaults.standard.set(uuidString, forKey: pairedDeviceUUIDKey)
UserDefaults.standard.synchronize()
os_log("已保存配对设备UUID: %{public}@", log: logger, type: .info, uuidString)
}
/// 清除已保存的配对设备UUID
private func clearPairedMacAddress() {
UserDefaults.standard.removeObject(forKey: pairedDeviceUUIDKey)
UserDefaults.standard.synchronize()
os_log("已清除配对设备UUID", log: logger, type: .info)
}
}

93
local_plugins/ble_service/ios/Classes/BleConst.swift

@ -1,93 +0,0 @@
import Foundation
import CoreBluetooth
/**
* BLE服务常量
*/
class BleConst {
// ================== 协议相关UUID ==================
/** 主服务UUID - 文档中定义为0000ABC0-0000-1111-2222-123456789ABC */
static let PRIMARY_SERVICE_UUID = CBUUID(string: "0000abc0-0000-1111-2222-123456789abc")
/** 音频服务UUID - 文档中定义为00001801-0000-1000-8000-00805f9b34fb */
static let AUDIO_SERVICE_UUID = CBUUID(string: "0000ae00-0000-1000-8000-00805f9b34fb")
/** 接收音频特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */
static let RECEIVE_AUDIO_CHAR_UUID = CBUUID(string: "0000ae02-0000-1000-8000-00805f9b34fb")
/** 写入特征UUID - 文档中定义为0000ABC1-0000-1111-2222-123456789ABC */
static let WRITE_CHAR_UUID = CBUUID(string: "0000abc1-0000-1111-2222-123456789abc")
/** 通知特征UUID - 文档中定义为0000ABC2-0000-1111-2222-123456789ABC */
static let NOTIFY_CHAR_UUID = CBUUID(string: "0000abc2-0000-1111-2222-123456789abc")
/** 客户端特征配置描述符UUID */
static let CLIENT_CHAR_CONFIG_UUID = CBUUID(string: "00002902-0000-1000-8000-00805f9b34fb")
/** BLE扫描Action */
static let ACTION_SCAN_RESULT = "com.yunqiinnovation.ble_service.ACTION_BLE_SCAN"
// ================== 连接状态 ==================
static let STATE_DISCONNECTED = 0
static let STATE_CONNECTING = 1
static let STATE_CONNECTED = 2
static let STATE_DISCONNECTING = 3
static let STATE_ERROR = 4
// ================== 广播厂商信息 ==================
/** 厂商ID - 文档中定义为0xABEF */
static let MANUFACTURER_ID: UInt16 = 0xABEF
/** 客户ID - 文档中定义为0xA001 */
static let CUSTOMER_ID: UInt16 = 0xA001
/** 产品ID - 文档中定义为0xA001 */
static let PRODUCT_ID: UInt16 = 0xA001
/** 蓝牙状态-未连接 */
static let BT_STATE_DISCONNECTED: UInt8 = 0x00
/** 蓝牙状态-已连接 */
static let BT_STATE_CONNECTED: UInt8 = 0x0C
// ================== 协议格式 ==================
/** APP请求帧头 */
static let APP_REQUEST_HEADER: UInt8 = 0xAA
/** 设备响应帧头 */
static let DEVICE_RESPONSE_HEADER: UInt8 = 0xBB
/** 设备主动上报帧头 */
static let DEVICE_NOTIFY_HEADER: UInt8 = 0xCC
// ================== 协议指令 (CMD) ==================
/** 获取耳机版本号 */
static let CMD_GET_VERSION: UInt8 = 0x01
/** 获取耳机产品ID */
static let CMD_GET_PRODUCT_ID: UInt8 = 0x02
/** 获取耳机颜色ID */
static let CMD_GET_COLOR_ID: UInt8 = 0x03
/** 获取耳机电量ID */
static let CMD_GET_BATTERY_INFO: UInt8 = 0x04
/** APP控制编解码指令 */
static let CMD_CONTROL_CODEC: UInt8 = 0x05
/** 音量增大 */
static let CMD_VOLUME_UP: UInt8 = 0x06
/** 音量减小 */
static let CMD_VOLUME_DOWN: UInt8 = 0x07
/** A2DP播放 */
static let CMD_PLAY_A2DP: UInt8 = 0x08
/** 唤醒指令 */
static let CMD_WAKE_UP: UInt8 = 0x11
// ================== 编解码控制值 ==================
/** 关闭编解码指令 */
static let CODEC_CONTROL_CLOSE: UInt8 = 0x00
/** 音乐或者通话远端声音 */
static let CODEC_CONTROL_DECODE_ON: UInt8 = 0xA1
/** mic和dac(音乐或者通话远端)声音 */
static let CODEC_CONTROL_A2DP_PLAY: UInt8 = 0xA2
/** 打开编码指令 */
static let CODEC_CONTROL_ENCODE_ON: UInt8 = 0xB1
/** 左声道 */
static let AUDIO_CHANNEL_LEFT: UInt8 = 0x01
/** 右声道 */
static let AUDIO_CHANNEL_RIGHT: UInt8 = 0x02
/** 立体声 */
static let AUDIO_CHANNEL_STEREO: UInt8 = 0x03
}

1266
local_plugins/ble_service/ios/Classes/BleService.swift

File diff suppressed because it is too large

4
local_plugins/ble_service/ios/Classes/BleServicePlugin.h

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

12
local_plugins/ble_service/ios/Classes/BleServicePlugin.m

@ -1,12 +0,0 @@
#import "BleServicePlugin.h"
#if __has_include(<ble_service/ble_service-Swift.h>)
#import <ble_service/ble_service-Swift.h>
#else
#import "ble_service-Swift.h"
#endif
@implementation BleServicePlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
[SwiftBleServicePlugin registerWithRegistrar:registrar];
}
@end

374
local_plugins/ble_service/ios/Classes/SwiftBleServicePlugin.swift

@ -1,374 +0,0 @@
import Flutter
import UIKit
import CoreBluetooth
import os
@available(iOS 13.0, *)
public class SwiftBleServicePlugin: NSObject, FlutterPlugin {
// MARK: - 属性
private var methodChannel: FlutterMethodChannel
internal var dataEventSink: FlutterEventSink?
internal var statusEventSink: FlutterEventSink?
private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "BleServicePlugin")
// 标记是否已初始化所有服务
private var servicesInitialized = false
// MARK: - 初始化方法
init(_ channel: FlutterMethodChannel) {
self.methodChannel = channel
super.init()
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
// iOS 18环境下,只设置回调但不主动初始化会话
let bleAccessorySetup = BleAccessorySetup.shared
// 在accessoryAdded回调中延迟设置BleService
bleAccessorySetup.setDeviceDiscoveredCallback { [weak self] uuid in
// 确保服务已初始化
self?.ensureServicesInitialized()
// 然后处理发现的设备
self?.handleDiscoveredDevice(uuid: uuid)
}
// 注释掉自动初始化,改为等待用户主动调用startDeviceAssociation
// bleAccessorySetup.initializeSession()
os_log("iOS 18环境:已设置设备发现回调,等待用户主动调用关联", log: logger, type: .info)
} else {
// 低于iOS 18,直接初始化所有服务
initializeAllServices()
}
#else
// 非AccessorySetupKit环境,直接初始化所有服务
initializeAllServices()
#endif
}
// 确保所有服务都已初始化
private func ensureServicesInitialized() {
if !servicesInitialized {
// 设置BLE服务回调
BleService.shared.addCallback(self)
// 设置BleService到AccessorySetup
BleAccessorySetup.shared.setBleService(BleService.shared)
servicesInitialized = true
}
}
// 初始化所有服务(iOS 17及以下版本的标准行为)
private func initializeAllServices() {
// 设置BLE服务回调
BleService.shared.addCallback(self)
// 初始化AccessorySetup,但不自动开始会话
BleAccessorySetup.shared.setBleService(BleService.shared)
BleAccessorySetup.shared.setDeviceDiscoveredCallback { [weak self] uuid in
self?.handleDiscoveredDevice(uuid: uuid)
}
// 移除自动初始化,统一由startDeviceAssociation方法触发
// BleAccessorySetup.shared.initializeSession()
os_log("非iOS 18环境:已设置BLE服务但不自动初始化会话", log: logger, type: .info)
servicesInitialized = true
}
// MARK: - FlutterPlugin Protocol
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "com.yunqiinnovation.ble_service", binaryMessenger: registrar.messenger())
let instance = SwiftBleServicePlugin(channel)
registrar.addMethodCallDelegate(instance, channel: channel)
// 注册事件通道 - 为每个通道创建独立的handler
let dataEventChannel = FlutterEventChannel(name: "com.yunqiinnovation.ble_service/data_events", binaryMessenger: registrar.messenger())
let statusEventChannel = FlutterEventChannel(name: "com.yunqiinnovation.ble_service/status_events", binaryMessenger: registrar.messenger())
// 创建独立的StreamHandler
let dataHandler = BleDataStreamHandler(instance)
let statusHandler = BleStatusStreamHandler(instance)
dataEventChannel.setStreamHandler(dataHandler)
statusEventChannel.setStreamHandler(statusHandler)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
// 检查是否需要初始化服务
checkAndInitializeServices()
switch call.method {
case "isConnected":
result(BleService.shared.isConnected())
case "getBatteryLevel":
// 获取电池信息,通过回调返回结果
if BleService.shared.getBatteryInfo() {
result(0) // 通过回调返回实际值
} else {
result(FlutterError(code: "UNAVAILABLE",
message: "无法获取电池信息",
details: nil))
}
case "isCompanionSupported":
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
result(true)
} else {
result(false)
}
#else
result(false)
#endif
case "getPairedMacAddress":
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
let pairedUUID = BleAccessorySetup.shared.getPairedMacAddress()
result(pairedUUID)
} else {
result(nil)
}
#else
result(nil)
#endif
case "isBleConnected":
result(BleService.shared.isConnected())
case "registerBackgroundScan", "unregisterBackgroundScan":
// iOS上禁用,直接返回成功以避免Flutter端报错
result(true)
case "getDeviceVersion":
result(BleService.shared.getDeviceVersion())
case "getProductId":
result(BleService.shared.getProductId())
case "getBatteryInfo":
result(BleService.shared.getBatteryInfo())
case "openEncoder":
result(BleService.shared.openEncoder())
case "openDecoder":
result(BleService.shared.openDecoder())
case "openA2DPDecoder":
result(BleService.shared.openA2DPDecoder())
case "closeCodec":
result(BleService.shared.closeCodec())
case "playA2DP":
result(BleService.shared.playA2DP())
// iOS 18+ AccessorySetupKit相关方法
case "clearAssociations":
// 清除设备配对关系
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
os_log("清除设备配对关系", log: logger, type: .info)
BleAccessorySetup.shared.clearAssociations()
result(true)
} else {
result(true)
}
#else
result(true)
#endif
case "startDeviceAssociation":
// 用户主动触发设备关联流程
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
os_log("用户主动触发设备关联流程", log: logger, type: .info)
// 确保服务已初始化
ensureServicesInitialized()
// 重新初始化会话
BleAccessorySetup.shared.initializeSession()
// 返回成功,实际配对结果会通过回调通知
result(true)
} else {
os_log("当前iOS版本不支持AccessorySetupKit", log: logger, type: .info)
result(false)
}
#else
os_log("当前环境不支持AccessorySetupKit", log: logger, type: .info)
result(false)
#endif
default:
result(FlutterMethodNotImplemented)
}
}
// MARK: - 私有方法
// 检查并按需初始化服务
private func checkAndInitializeServices() {
#if canImport(AccessorySetupKit) && swift(>=5.9)
if #available(iOS 18.0, *) {
ensureServicesInitialized()
}
#endif
}
// AccessorySetupKit发现设备的处理
private func handleDiscoveredDevice(uuid: UUID) {
// BleAccessorySetup内部已进行连接,此处不再重复调用
// os_log("收到设备发现回调: %{public}@", log: logger, type: .info, uuid.uuidString)
// 可以在这里添加设备记录或其他操作
}
private func sendEvent<T>(_ eventSink: FlutterEventSink?, data: T, errorPrefix: String) {
if let eventSink = eventSink {
DispatchQueue.main.async {
eventSink(data)
}
}
}
// 获取顶层视图控制器的辅助方法
private func getTopViewController(_ viewController: UIViewController) -> UIViewController {
if let presented = viewController.presentedViewController {
return getTopViewController(presented)
}
if let tabBarController = viewController as? UITabBarController,
let selected = tabBarController.selectedViewController {
return getTopViewController(selected)
}
if let navigationController = viewController as? UINavigationController,
let visibleViewController = navigationController.visibleViewController {
return getTopViewController(visibleViewController)
}
return viewController
}
}
// MARK: - BleService.Callback
@available(iOS 13.0, *)
extension SwiftBleServicePlugin: BleService.Callback {
public func onScanResult(devices: [[String: Any]]) {
if let eventSink = statusEventSink {
DispatchQueue.main.async {
eventSink([
"type": "scanResult",
"devices": devices
])
}
}
}
public func onConnectionStateChanged(state: Int) {
if let eventSink = statusEventSink {
var stateStr = "Unknown"
switch state {
case BleConst.STATE_DISCONNECTED:
stateStr = "Disconnected"
case BleConst.STATE_CONNECTING:
stateStr = "Connecting"
case BleConst.STATE_CONNECTED:
stateStr = "Connected"
case BleConst.STATE_DISCONNECTING:
stateStr = "Disconnecting"
case BleConst.STATE_ERROR:
stateStr = "Connection Error"
default:
stateStr = "Unknown State(\(state))"
}
DispatchQueue.main.async {
eventSink([
"type": "connectionStateChanged",
"state": state,
"stateStr": stateStr
])
}
}
}
public func onAudioDataReceived(data: Data) {
if let eventSink = dataEventSink {
DispatchQueue.main.async {
eventSink([
"type": "audioData",
"data": FlutterStandardTypedData(bytes: data)
])
}
}
}
public func onWakeupSignalReceived() {
if let eventSink = statusEventSink {
DispatchQueue.main.async {
eventSink([
"type": "wakeup"
])
}
}
}
public func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) {
if let eventSink = statusEventSink {
var resultMap = infoData
resultMap["type"] = "deviceInfo"
resultMap["result"] = infoType
DispatchQueue.main.async {
eventSink(resultMap)
}
}
}
}
// MARK: - 独立的StreamHandler类
@available(iOS 13.0, *)
class BleDataStreamHandler: NSObject, FlutterStreamHandler {
private weak var plugin: SwiftBleServicePlugin?
init(_ plugin: SwiftBleServicePlugin) {
self.plugin = plugin
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
plugin?.dataEventSink = events
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
plugin?.dataEventSink = nil
return nil
}
}
@available(iOS 13.0, *)
class BleStatusStreamHandler: NSObject, FlutterStreamHandler {
private weak var plugin: SwiftBleServicePlugin?
init(_ plugin: SwiftBleServicePlugin) {
self.plugin = plugin
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
plugin?.statusEventSink = events
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
plugin?.statusEventSink = nil
return nil
}
}

239
local_plugins/ble_service/ios/Classes/SwiftOpusAudioProcessor.swift

@ -1,239 +0,0 @@
import Foundation
import AVFoundation
import YbridOpus
@available(iOS 13.0, *)
class SwiftOpusAudioProcessor: NSObject {
protocol AudioDataCallback: AnyObject {
func onAudioDataReceived(data: Data)
}
private var isDecodingActive = false
private weak var callback: AudioDataCallback?
// Opus 解码器相关 - 使用 YbridOpus C API
private var decoder: OpaquePointer?
private var sampleRate: Int32 = 16000
private var channels: Int32 = 1
private let maxFrameSize: Int32 = 5760 // 120 ms @48 kHz 的最大值
override init() {
super.init()
}
deinit {
cleanup()
}
private func initializeOpusDecoder() -> Bool {
if decoder != nil {
return true
}
var error: Int32 = 0
decoder = opus_decoder_create(sampleRate, channels, &error)
guard error == OPUS_OK, decoder != nil else {
return false
}
return true
}
private func destroyOpusDecoder() {
if let decoder = decoder {
opus_decoder_destroy(decoder)
self.decoder = nil
}
}
func setCallback(_ callback: AudioDataCallback?) {
self.callback = callback
}
func startDecoding(sampleRate: Int32 = 16000, channels: Int32 = 1, frameSize: Int32 = 640) -> Bool {
if isDecodingActive {
return true
}
self.sampleRate = sampleRate
self.channels = channels
guard initializeOpusDecoder() else {
return false
}
isDecodingActive = true
return true
}
func stopDecoding() -> Bool {
if !isDecodingActive {
return false
}
isDecodingActive = false
destroyOpusDecoder()
return true
}
func processAudioData(_ data: Data) {
guard isDecodingActive, let decoder = decoder, data.count > 0 else {
return
}
// 增强的数据包验证
guard data.count <= 1275 else {
print("音频数据包太大: \(data.count) bytes,跳过处理")
return
}
// 检查最小数据包大小(Opus最小包通常至少几个字节)
guard data.count >= 3 else {
print("音频数据包太小: \(data.count) bytes,跳过处理")
return
}
// 简单的 Opus 头部验证(检查是否是有效的 Opus 数据包格式)
let firstByte = data[0]
// Opus packets start with a TOC (Table of Contents) byte
// 检查TOC字节是否在合理范围内
if firstByte == 0x00 || firstByte == 0xFF {
print("可疑的Opus数据包头部: 0x\(String(format: "%02X", firstByte)),跳过处理")
return
}
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
self?.decodeOpusPacket(data)
}
}
// 简化的 Opus 解码方法,重点是容错处理
private func decodeOpusPacket(_ packet: Data) {
guard let decoder = decoder else { return }
let maxSamples = Int(maxFrameSize)
var pcmBuffer = [opus_int16](repeating: 0, count: maxSamples * Int(channels))
let decodedSamples = packet.withUnsafeBytes { packetPtr in
let uint8Ptr = packetPtr.bindMemory(to: UInt8.self).baseAddress
return opus_decode(
decoder,
uint8Ptr,
opus_int32(packet.count),
&pcmBuffer,
Int32(maxSamples),
0
)
}
// 增强的错误处理:检查各种错误情况
if decodedSamples <= 0 {
let errorCode = decodedSamples
print("Opus解码失败,错误代码: \(errorCode),重新初始化解码器")
// 根据错误类型进行不同的处理
if errorCode == -1 { // OPUS_BAD_ARG
print("Opus解码参数错误,执行完全重置")
performCompleteReset()
} else if errorCode == -2 { // OPUS_BUFFER_TOO_SMALL
print("Opus缓冲区太小")
resetDecoder()
} else if errorCode == -3 { // OPUS_INTERNAL_ERROR
print("Opus内部错误,执行完全重置")
performCompleteReset()
} else {
print("Opus其他错误,尝试简单重置")
resetDecoder()
}
return
}
// 验证解码结果的合理性
guard decodedSamples <= maxFrameSize else {
print("Opus解码样本数超出预期: \(decodedSamples) > \(maxFrameSize)")
resetDecoder()
return
}
// 提取解码的音频数据
let samplesPerChannel = Int(decodedSamples)
let totalSamples = samplesPerChannel * Int(channels)
// 验证总样本数的合理性
guard totalSamples <= pcmBuffer.count else {
print("计算的总样本数超出缓冲区: \(totalSamples) > \(pcmBuffer.count)")
resetDecoder()
return
}
let outputPCM = Array(pcmBuffer[0..<totalSamples])
if !outputPCM.isEmpty {
processPCMData(outputPCM)
}
}
// 重置解码器的简单方法
private func resetDecoder() {
print("执行简单解码器重置")
destroyOpusDecoder()
_ = initializeOpusDecoder()
}
// 执行完全重置(包括停止和重启解码)
private func performCompleteReset() {
print("执行完全解码器重置")
let wasActive = isDecodingActive
let currentSampleRate = self.sampleRate
let currentChannels = self.channels
// 完全停止解码
if wasActive {
isDecodingActive = false
destroyOpusDecoder()
}
// 重新初始化所有状态
self.sampleRate = currentSampleRate
self.channels = currentChannels
// 如果之前是活跃状态,重新启动
if wasActive {
guard initializeOpusDecoder() else {
print("完全重置后重新初始化失败")
return
}
isDecodingActive = true
print("完全重置成功,解码器已重新启动")
}
}
private func processPCMData(_ pcmData: [opus_int16]) {
// 转换为 Data
let data = Data(bytes: pcmData, count: pcmData.count * MemoryLayout<opus_int16>.size)
// 回调通知
DispatchQueue.main.async { [weak self] in
self?.callback?.onAudioDataReceived(data: data)
}
}
func isDecoding() -> Bool {
return isDecodingActive
}
func cleanup() {
stopDecoding()
}
// MARK: - 公共接口
/// 获取当前配置信息
func getCurrentConfig() -> (sampleRate: Int32, channels: Int32) {
return (sampleRate, channels)
}
}

1
local_plugins/ble_service/ios/Classes/XCFrameworkExample.swift

@ -1 +0,0 @@
Loading…
Cancel
Save