|
|
@ -26,6 +26,8 @@ enum AudioOutputDevice { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
class TranslationController extends GetxController { |
|
|
class TranslationController extends GetxController { |
|
|
|
|
|
static const String TAG = 'TranslationController'; |
|
|
|
|
|
|
|
|
// 服务 |
|
|
// 服务 |
|
|
final AsrService _asrService = Get.find<AsrService>(); |
|
|
final AsrService _asrService = Get.find<AsrService>(); |
|
|
final VolcanoTranslationService _translationService = |
|
|
final VolcanoTranslationService _translationService = |
|
|
@ -56,6 +58,14 @@ class TranslationController extends GetxController { |
|
|
// 主页面显示限制:只显示最近20条,保证界面性能和用户体验 |
|
|
// 主页面显示限制:只显示最近20条,保证界面性能和用户体验 |
|
|
static const int _maxDisplayItems = 20; |
|
|
static const int _maxDisplayItems = 20; |
|
|
|
|
|
|
|
|
|
|
|
// 分页加载相关状态 |
|
|
|
|
|
final isLoadingMore = false.obs; // 是否正在加载更多历史记录 |
|
|
|
|
|
final hasMoreHistory = true.obs; // 是否还有更多历史记录 |
|
|
|
|
|
final currentPage = 1.obs; // 当前页码 |
|
|
|
|
|
final pageSize = 20; // 每页记录数量 |
|
|
|
|
|
final isLoadingMoreError = false.obs; // 加载更多时是否出错 |
|
|
|
|
|
bool _scrollListenerSetup = false; // 滚动监听器设置标志 |
|
|
|
|
|
|
|
|
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; // 上一次状态 |
|
|
// 状态变量 |
|
|
// 状态变量 |
|
|
@ -178,10 +188,8 @@ class TranslationController extends GetxController { |
|
|
// 初始化服务 |
|
|
// 初始化服务 |
|
|
_initServices(); |
|
|
_initServices(); |
|
|
|
|
|
|
|
|
// 使用微任务在布局完成后自动滚动到底部 |
|
|
// 🔑 reverse模式下不需要手动滚动,ListView会自然显示最新内容 |
|
|
WidgetsBinding.instance.addPostFrameCallback((_) { |
|
|
// 参考Agent模块的实现模式 |
|
|
_scrollToBottom(); |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 加载保存的语言设置 |
|
|
// 加载保存的语言设置 |
|
|
@ -724,18 +732,28 @@ class TranslationController extends GetxController { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 滚动到底部 |
|
|
// 滚动到底部(适配reverse模式) |
|
|
void _scrollToBottom() { |
|
|
void _scrollToBottom({bool animate = true}) { |
|
|
if (scrollController.hasClients && |
|
|
if (scrollController.hasClients && |
|
|
(translationHistory.isNotEmpty || currentSessionId != null)) { |
|
|
(translationHistory.isNotEmpty || currentSessionId != null)) { |
|
|
try { |
|
|
try { |
|
|
scrollController.animateTo( |
|
|
// 🔑 reverse模式下:滚动到顶部(position = 0)来显示最新消息 |
|
|
scrollController.position.maxScrollExtent, |
|
|
// 正常模式下:滚动到底部(maxScrollExtent)来显示最新消息 |
|
|
duration: const Duration(milliseconds: 300), |
|
|
const targetPosition = 0.0; // reverse模式下最新消息在顶部 |
|
|
curve: Curves.easeOut, |
|
|
|
|
|
); |
|
|
if (animate) { |
|
|
|
|
|
// 有动画的滚动,用于新消息添加时 |
|
|
|
|
|
scrollController.animateTo( |
|
|
|
|
|
targetPosition, |
|
|
|
|
|
duration: const Duration(milliseconds: 300), |
|
|
|
|
|
curve: Curves.easeOut, |
|
|
|
|
|
); |
|
|
|
|
|
} else { |
|
|
|
|
|
// 无动画的跳转,用于初始化时 |
|
|
|
|
|
scrollController.jumpTo(targetPosition); |
|
|
|
|
|
} |
|
|
} catch (e) { |
|
|
} catch (e) { |
|
|
Logger.error('滚动到底部失败: ${e.toString()}'); |
|
|
Logger.error('滚动到顶部失败(reverse模式): ${e.toString()}'); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
@ -849,22 +867,37 @@ class TranslationController extends GetxController { |
|
|
// 【注意】存储中的记录都是完成态记录(isIntermediate = false) |
|
|
// 【注意】存储中的记录都是完成态记录(isIntermediate = false) |
|
|
// 因为保存时已经过滤掉了中间态记录,确保数据质量 |
|
|
// 因为保存时已经过滤掉了中间态记录,确保数据质量 |
|
|
|
|
|
|
|
|
|
|
|
// 初始化分页状态 |
|
|
|
|
|
currentPage.value = 1; |
|
|
|
|
|
isLoadingMore.value = false; |
|
|
|
|
|
isLoadingMoreError.value = false; |
|
|
|
|
|
|
|
|
// 主页面只显示最近的20条完成态记录,保证界面性能 |
|
|
// 主页面只显示最近的20条完成态记录,保证界面性能 |
|
|
if (allHistory.length > _maxDisplayItems) { |
|
|
if (allHistory.length > _maxDisplayItems) { |
|
|
translationHistory.value = |
|
|
translationHistory.value = |
|
|
allHistory.sublist(allHistory.length - _maxDisplayItems); |
|
|
allHistory.sublist(allHistory.length - _maxDisplayItems); |
|
|
|
|
|
// 检查是否还有更多历史记录 |
|
|
|
|
|
hasMoreHistory.value = true; |
|
|
} else { |
|
|
} else { |
|
|
translationHistory.value = allHistory; |
|
|
translationHistory.value = allHistory; |
|
|
|
|
|
hasMoreHistory.value = false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// 使用微任务确保在视图完全渲染后滚动到底部 |
|
|
// 设置滚动监听器 |
|
|
WidgetsBinding.instance.addPostFrameCallback((_) { |
|
|
_setupScrollListener(); |
|
|
_scrollToBottom(); |
|
|
|
|
|
}); |
|
|
// 🔑 reverse模式下不需要滚动到底部,ListView会自然显示最新记录 |
|
|
|
|
|
// 参考Agent模块:使用reverse:true时不需要手动滚动到底部 |
|
|
} catch (e) { |
|
|
} catch (e) { |
|
|
_storage.remove(_historyKey); |
|
|
_storage.remove(_historyKey); |
|
|
Logger.error('加载翻译历史失败: ${e.toString()}'); |
|
|
Logger.error('加载翻译历史失败: ${e.toString()}'); |
|
|
} |
|
|
} |
|
|
|
|
|
} else { |
|
|
|
|
|
// 没有历史数据时也要初始化分页状态 |
|
|
|
|
|
hasMoreHistory.value = false; |
|
|
|
|
|
currentPage.value = 1; |
|
|
|
|
|
isLoadingMore.value = false; |
|
|
|
|
|
isLoadingMoreError.value = false; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -894,6 +927,109 @@ class TranslationController extends GetxController { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 设置滚动监听器(适配reverse模式) |
|
|
|
|
|
void _setupScrollListener() { |
|
|
|
|
|
if (_scrollListenerSetup) return; // 防止重复设置 |
|
|
|
|
|
|
|
|
|
|
|
scrollController.addListener(() { |
|
|
|
|
|
// 🔑 reverse模式下:检查是否滚动到底部(实际是历史记录的顶部) |
|
|
|
|
|
// 参考Agent模块的实现 |
|
|
|
|
|
if (scrollController.position.pixels >= scrollController.position.maxScrollExtent - 100) { |
|
|
|
|
|
// 当距离底部100像素时触发加载更多历史记录 |
|
|
|
|
|
if (hasMoreHistory.value && !isLoadingMore.value) { |
|
|
|
|
|
Logger.d(TAG, '触发加载更多翻译历史记录(reverse模式)'); |
|
|
|
|
|
loadMoreHistory(); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
_scrollListenerSetup = true; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 加载更多历史记录(分页加载) |
|
|
|
|
|
Future<void> loadMoreHistory() async { |
|
|
|
|
|
// 防止重复加载 |
|
|
|
|
|
if (isLoadingMore.value || !hasMoreHistory.value) { |
|
|
|
|
|
Logger.d(TAG, '跳过加载更多:isLoadingMore=${isLoadingMore.value}, hasMoreHistory=${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 totalPages = (totalItems / pageSize).ceil(); |
|
|
|
|
|
|
|
|
|
|
|
// 修复分页索引计算:从最新记录向前分页 |
|
|
|
|
|
// 第1页:最新20条 (index: totalItems-20 到 totalItems) |
|
|
|
|
|
// 第2页:接下来20条 (index: totalItems-40 到 totalItems-20) |
|
|
|
|
|
// 第3页:再接下来20条 (index: totalItems-60 到 totalItems-40) |
|
|
|
|
|
final endIndex = totalItems - ((nextPage - 1) * pageSize); |
|
|
|
|
|
final startIndex = totalItems - (nextPage * pageSize); |
|
|
|
|
|
|
|
|
|
|
|
Logger.d(TAG, '分页计算: 总记录=$totalItems, 总页数=$totalPages, 当前页=$nextPage, startIndex=$startIndex, endIndex=$endIndex'); |
|
|
|
|
|
|
|
|
|
|
|
if (endIndex > 0) { |
|
|
|
|
|
// 获取当前页的数据,确保索引不越界 |
|
|
|
|
|
final actualStartIndex = startIndex < 0 ? 0 : startIndex; |
|
|
|
|
|
final actualEndIndex = endIndex > totalItems ? totalItems : endIndex; |
|
|
|
|
|
|
|
|
|
|
|
final pageItems = allHistory.sublist(actualStartIndex, actualEndIndex); |
|
|
|
|
|
Logger.d(TAG, '实际索引: start=$actualStartIndex, end=$actualEndIndex, 获取${pageItems.length}条记录'); |
|
|
|
|
|
|
|
|
|
|
|
if (pageItems.isNotEmpty) { |
|
|
|
|
|
// 🔑 简化延迟,参考Agent模块(300ms vs 1000ms) |
|
|
|
|
|
await Future.delayed(const Duration(milliseconds: 300)); |
|
|
|
|
|
|
|
|
|
|
|
// 🔑 reverse模式下:将新记录插入到现有记录列表的开头 |
|
|
|
|
|
// 由于reverse显示,用户看到的顺序不会改变,无需复杂的位置保持 |
|
|
|
|
|
final updatedHistory = [...pageItems, ...translationHistory]; |
|
|
|
|
|
translationHistory.assignAll(updatedHistory); |
|
|
|
|
|
|
|
|
|
|
|
// 更新分页状态 |
|
|
|
|
|
currentPage.value = nextPage; |
|
|
|
|
|
// 修复hasMoreHistory判断:当startIndex <= 0时表示已经到达最早的记录 |
|
|
|
|
|
hasMoreHistory.value = startIndex > 0; |
|
|
|
|
|
|
|
|
|
|
|
Logger.i(TAG, |
|
|
|
|
|
'成功加载 ${pageItems.length} 条历史记录,当前页: $nextPage/$totalPages, 还有更多: ${hasMoreHistory.value}'); |
|
|
|
|
|
} else { |
|
|
|
|
|
// 没有更多记录 |
|
|
|
|
|
hasMoreHistory.value = false; |
|
|
|
|
|
Logger.i(TAG, '没有更多翻译历史记录'); |
|
|
|
|
|
} |
|
|
|
|
|
} else { |
|
|
|
|
|
// 索引超出范围,没有更多记录 |
|
|
|
|
|
hasMoreHistory.value = false; |
|
|
|
|
|
Logger.i(TAG, '已到达历史记录开头'); |
|
|
|
|
|
} |
|
|
|
|
|
} else { |
|
|
|
|
|
// 没有存储数据 |
|
|
|
|
|
hasMoreHistory.value = false; |
|
|
|
|
|
Logger.i(TAG, '存储中没有更多历史记录'); |
|
|
|
|
|
} |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
Logger.e(TAG, '加载更多历史记录失败: $e'); |
|
|
|
|
|
isLoadingMoreError.value = true; |
|
|
|
|
|
} finally { |
|
|
|
|
|
isLoadingMore.value = false; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 获取所有完成态历史记录(供历史页面使用) |
|
|
// 获取所有完成态历史记录(供历史页面使用) |
|
|
// 【修复】简化实现,合并存储和内存中的完成态记录 |
|
|
// 【修复】简化实现,合并存储和内存中的完成态记录 |
|
|
List<TranslationItem> getAllHistory() { |
|
|
List<TranslationItem> getAllHistory() { |
|
|
@ -1024,7 +1160,7 @@ class TranslationController extends GetxController { |
|
|
|
|
|
|
|
|
// 滚动到底部 (公共方法,用于向后兼容) |
|
|
// 滚动到底部 (公共方法,用于向后兼容) |
|
|
void scrollToBottom({bool animate = true}) { |
|
|
void scrollToBottom({bool animate = true}) { |
|
|
_scrollToBottom(); |
|
|
_scrollToBottom(animate: animate); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|