diff --git a/apps/client/lib/core/utils/recent_languages.dart b/apps/client/lib/core/utils/recent_languages.dart new file mode 100644 index 00000000..c5114ef8 --- /dev/null +++ b/apps/client/lib/core/utils/recent_languages.dart @@ -0,0 +1,58 @@ +import 'package:get_storage/get_storage.dart'; + +/// 语言选择弹窗的「最近使用」排序。 +/// +/// 语言表本身有几十项(面对面/同传/音视频共用 LanguageManager 那份), +/// 每次都从头翻很慢,所以用过的往上提,最顶的是最近一次选的。 +/// +/// ⚠️ 源/目标**分开记**:同一个人的源语言常年是母语、目标语言才是变的那个, +/// 合成一份会让刚选的目标语言跑到源语言列表顶上,越用越乱。 +class RecentLanguages { + RecentLanguages._(); + + /// 每侧最多记几个。再多就没有「置顶」的意义了,整张表都成了最近使用。 + static const int maxKeep = 8; + + static const String _sourceKey = 'recent_languages_source'; + static const String _targetKey = 'recent_languages_target'; + + static String _keyOf(bool isSource) => isSource ? _sourceKey : _targetKey; + + static final GetStorage _storage = GetStorage(); + + /// 读最近使用列表(最新的在前)。存储里可能是任意类型,逐项转字符串兜底。 + static List read(bool isSource) { + final raw = _storage.read(_keyOf(isSource)); + if (raw is! List) return []; + return raw + .map((e) => e?.toString() ?? '') + .where((e) => e.isNotEmpty) + .toList(); + } + + /// 记一次使用:已存在就提到最前,不重复占位。 + static void touch(String language, {required bool isSource}) { + if (language.isEmpty) return; + final list = read(isSource)..remove(language); + list.insert(0, language); + if (list.length > maxKeep) list.removeRange(maxKeep, list.length); + _storage.write(_keyOf(isSource), list); + } + + /// 按最近使用重排:用过的按使用顺序排在前面,其余保持原有顺序。 + /// + /// 只提「本次列表里确实有」的语言 —— 各模块传进来的语言表不一样 + /// (语音翻译走 LanguageManager、图片翻译另有一份), + /// 直接把记录拼上去会冒出当前模式根本不支持的语言。 + static List sort(List languages, {required bool isSource}) { + final recent = read(isSource); + if (recent.isEmpty) return languages; + + final remaining = List.from(languages); + final ordered = []; + for (final name in recent) { + if (remaining.remove(name)) ordered.add(name); + } + return [...ordered, ...remaining]; + } +} diff --git a/apps/client/lib/modules/translation/controllers/translation_controller.dart b/apps/client/lib/modules/translation/controllers/translation_controller.dart index 4eb0ebe9..07bf065c 100644 --- a/apps/client/lib/modules/translation/controllers/translation_controller.dart +++ b/apps/client/lib/modules/translation/controllers/translation_controller.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; +import '../../../core/utils/recent_languages.dart'; import 'package:intl/intl.dart'; import 'package:floating_ui_plugin/native_plugin.dart'; import 'package:path_provider/path_provider.dart'; @@ -2787,23 +2788,30 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } /// 切换源语言和目标语言 - Future swapLanguages() => _runLanguageSwitch(() async { - if (!guardLanguageChange()) return; - final wasRecognizing = isRecognizing.value; - await stopAll(); + Future swapLanguages() => _runLanguageSwitch(_swapLanguagesInner); - final tempName = sourceLanguage.value; - sourceLanguage.value = targetLanguage.value; - targetLanguage.value = tempName; + /// 对调的实现体。**不包 [_runLanguageSwitch]** —— 选语言撞上对侧时要在 + /// 已经排队进来的那一次切换里直接调它,再包一层会等自己前面那条链,死锁。 + Future _swapLanguagesInner() async { + if (!guardLanguageChange()) return; + final wasRecognizing = isRecognizing.value; + await stopAll(); - final tempCode = sourceLanguageCode.value; - sourceLanguageCode.value = targetLanguageCode.value; - targetLanguageCode.value = tempCode; + final tempName = sourceLanguage.value; + sourceLanguage.value = targetLanguage.value; + targetLanguage.value = tempName; - _saveSourceLanguageSetting(isUserSet: true); - _saveTargetLanguageSetting(); - await _reinitializeAsrService(restartRecognition: wasRecognizing); - }); + final tempCode = sourceLanguageCode.value; + sourceLanguageCode.value = targetLanguageCode.value; + targetLanguageCode.value = tempCode; + + RecentLanguages.touch(sourceLanguage.value, isSource: true); + RecentLanguages.touch(targetLanguage.value, isSource: false); + + _saveSourceLanguageSetting(isUserSet: true); + _saveTargetLanguageSetting(); + await _reinitializeAsrService(restartRecognition: wasRecognizing); + } /// 设置源语言 /// [language] 语言名称 @@ -2979,6 +2987,9 @@ class TranslationController extends GetxController with WidgetsBindingObserver { Logger.d(TAG, '触发加载更多翻译历史记录(reverse模式)'); loadMoreHistory(); } + // 选中的正是目标语言 → 两边对调,而不是让源=目标。 + // (语言弹窗左右两侧给的是同一张完整表,撞上是正常操作,见 LanguageSelectionDialog) + if (targetLanguage.value == language) return _swapLanguagesInner(); } }); @@ -2998,6 +3009,8 @@ class TranslationController extends GetxController with WidgetsBindingObserver { /// [timestamp] 时间戳 void deleteTranslationItem(String sessionId, DateTime timestamp) => _historyManager.deleteTranslationItem(sessionId, timestamp); + // 选中的正是源语言 → 两边对调(同上) + if (sourceLanguage.value == language) return _swapLanguagesInner(); /// 获取所有历史记录 /// 返回所有翻译项列表 diff --git a/apps/client/lib/modules/translation/views/dialogs/language_selection_dialog.dart b/apps/client/lib/modules/translation/views/dialogs/language_selection_dialog.dart index 3301ee72..ac6b4955 100644 --- a/apps/client/lib/modules/translation/views/dialogs/language_selection_dialog.dart +++ b/apps/client/lib/modules/translation/views/dialogs/language_selection_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../../../core/utils/recent_languages.dart'; import '../../controllers/translation_controller.dart'; /// 语言选择对话框 @@ -12,7 +13,11 @@ class LanguageSelectionDialog { /// [currentLanguage] 当前选中的语言中文名称 /// [includeAutoDetect] 是否在源语言首项加入“自动检测”选项(仅文本翻译需要) /// [translateLanguageNames] 是否将语言名称作为翻译键进行翻译(语音翻译保持原逻辑) - /// [excludeLanguage] 需排除的语言中文名称(防止原语言与目标语言相同) + /// [excludeLanguage] 需排除的语言中文名称。 + /// ⚠️ 语音翻译(同传/面对面/音视频)**不要传**:左右两侧看到的应该是同一张完整语言表, + /// 把对侧已选的那项从列表里删掉,会让人以为那个语言没了。源=目标由 controller 的 + /// setSourceLanguage/setTargetLanguage 在选中时**对调**来避免。 + /// 文本/图片翻译的源语言有「自动检测」,对调会把它挪进目标语言(非法值),故仍走排除。 /// [onSelected] 选中项回调,参数为中文名称 static void show( bool isSource, @@ -34,11 +39,15 @@ class LanguageSelectionDialog { final displayLanguages = List.from(languages ?? fallbackLanguages); - // 过滤掉对侧已选语言,防止原语言与目标语言相同 + // 过滤掉对侧已选语言,防止原语言与目标语言相同(仅文本/图片翻译传,见上方说明) if (excludeLanguage != null && excludeLanguage.isNotEmpty) { displayLanguages.remove(excludeLanguage); } + // 用过的语言置顶,最顶的是最近一次选的 + final orderedLanguages = + RecentLanguages.sort(displayLanguages, isSource: isSource); + // 源语言是否展示“自动检测”选项(仅列表显示,值为中文“自动检测”) final hasAutoDetect = includeAutoDetect && isSource; @@ -92,14 +101,14 @@ class LanguageSelectionDialog { mainAxisSize: MainAxisSize.min, children: List.generate( hasAutoDetect - ? (displayLanguages.length + 1) - : displayLanguages.length, + ? (orderedLanguages.length + 1) + : orderedLanguages.length, (index) { // 构造展示与实际值 final isAutoDetectItem = hasAutoDetect && index == 0; final valueText = isAutoDetectItem ? 'autoDetect'.tr - : displayLanguages[ + : orderedLanguages[ hasAutoDetect ? index - 1 : index]; final displayText = isAutoDetectItem ? 'autoDetect'.tr // 自动检测 @@ -112,6 +121,11 @@ class LanguageSelectionDialog { return GestureDetector( // 改用手势检测器实现点击 onTap: () { + // 记一次使用,下次这项排到最前(自动检测不是语言,不记) + if (!isAutoDetectItem) { + RecentLanguages.touch(valueText, + isSource: isSource); + } if (onSelected != null) { onSelected(valueText); } else { diff --git a/apps/client/lib/modules/translation/views/widgets/face_to_face_layout.dart b/apps/client/lib/modules/translation/views/widgets/face_to_face_layout.dart index b758b6ab..642afe57 100644 --- a/apps/client/lib/modules/translation/views/widgets/face_to_face_layout.dart +++ b/apps/client/lib/modules/translation/views/widgets/face_to_face_layout.dart @@ -260,10 +260,8 @@ class _FaceToFaceLayoutState extends State { void _showLanguageSelectionDialog(bool isSource, bool isDarkMode) { // 翻译进行中不开弹窗(见 controller.guardLanguageChange) if (!widget.controller.guardLanguageChange()) return; + // 不传 excludeLanguage:两侧列表保持一致,选到对侧语言时 controller 对调 LanguageSelectionDialog.show(isSource, isDarkMode, - excludeLanguage: isSource - ? widget.controller.targetLanguage.value - : widget.controller.sourceLanguage.value, translationController: widget.controller); } }