Browse Source

client: 语音翻译的语言弹窗两侧给同一张表,用过的置顶

此前左右两侧会把对侧已选的那项从列表里删掉(excludeLanguage),看起来像那个
语言没了。语音翻译(同传/面对面/音视频)不再传这个参数,选中对侧语言时由
controller 对调来避免源=目标。文本/图片翻译仍走排除——它们的源语言有「自动
检测」,对调会把这个非法值挪进目标语言。

- 对调实现体拆出 _swapLanguagesInner,不包 _runLanguageSwitch:撞上对侧时是
  在已经排队进来的那次切换里调它,再包一层会等自己前面那条链,死锁。
- RecentLanguages(GetStorage)记最近选过的语言并置顶,源/目标各一份列表。
  「自动检测」不是语言,不记。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
main
Rodger-Wang 3 weeks ago
parent
commit
7aef22905b
  1. 58
      apps/client/lib/core/utils/recent_languages.dart
  2. 17
      apps/client/lib/modules/translation/controllers/translation_controller.dart
  3. 24
      apps/client/lib/modules/translation/views/dialogs/language_selection_dialog.dart
  4. 4
      apps/client/lib/modules/translation/views/widgets/face_to_face_layout.dart

58
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<String> read(bool isSource) {
final raw = _storage.read(_keyOf(isSource));
if (raw is! List) return <String>[];
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<String> sort(List<String> languages, {required bool isSource}) {
final recent = read(isSource);
if (recent.isEmpty) return languages;
final remaining = List<String>.from(languages);
final ordered = <String>[];
for (final name in recent) {
if (remaining.remove(name)) ordered.add(name);
}
return [...ordered, ...remaining];
}
}

17
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,7 +2788,11 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
}
/// 切换源语言和目标语言
Future<void> swapLanguages() => _runLanguageSwitch(() async {
Future<void> swapLanguages() => _runLanguageSwitch(_swapLanguagesInner);
/// 对调的实现体。**不包 [_runLanguageSwitch]** —— 选语言撞上对侧时要在
/// 已经排队进来的那一次切换里直接调它,再包一层会等自己前面那条链,死锁。
Future<void> _swapLanguagesInner() async {
if (!guardLanguageChange()) return;
final wasRecognizing = isRecognizing.value;
await stopAll();
@ -2800,10 +2805,13 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
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();
/// 获取所有历史记录
/// 返回所有翻译项列表

24
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<String>.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 {

4
apps/client/lib/modules/translation/views/widgets/face_to_face_layout.dart

@ -260,10 +260,8 @@ class _FaceToFaceLayoutState extends State<FaceToFaceLayout> {
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);
}
}

Loading…
Cancel
Save