Browse Source

feat(translation): 优化面对面翻译布局添加可拖拽悬浮按钮

- 在翻译主页面添加可拖拽悬浮按钮,支持位置保存
- 重构面对面翻译模式布局,采用对称设计
- 增加翻译项日志打印便于调试
- 为绑定类添加中文注释说明
weicu
lxm 1 year ago
parent
commit
29678af58b
  1. 4
      lib/modules/translation/bindings/translation_binding.dart
  2. 6
      lib/modules/translation/bindings/translation_detail_binding.dart
  3. 1
      lib/modules/translation/bindings/translation_history_binding.dart
  4. 1
      lib/modules/translation/bindings/translation_list_binding.dart
  5. 3
      lib/modules/translation/bindings/translation_stats_binding.dart
  6. 45
      lib/modules/translation/controllers/translation_controller.dart
  7. 12
      lib/modules/translation/models/translation_models.dart
  8. 360
      lib/modules/translation/views/translation_view.dart

4
lib/modules/translation/bindings/translation_binding.dart

@ -1,10 +1,10 @@
import 'package:get/get.dart';
import '../controllers/translation_controller.dart';
//主翻译页面依赖绑定
class TranslationBinding implements Bindings {
@override
void dependencies() {
Get.put(TranslationController());
}
}
}

6
lib/modules/translation/bindings/translation_detail_binding.dart

@ -1,9 +1,11 @@
import 'package:get/get.dart';
import '../controllers/translation_detail_controller.dart';
//翻译详情页面依赖绑定
class TranslationDetailBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<TranslationDetailController>(() => TranslationDetailController());
Get.lazyPut<TranslationDetailController>(
() => TranslationDetailController());
}
}
}

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

@ -1,6 +1,7 @@
import 'package:get/get.dart';
import '../controllers/translation_audiohistory_controller.dart';
//翻译历史记录页面依赖绑定
class TranslationHistoryBinding extends Bindings {
@override
void dependencies() {

1
lib/modules/translation/bindings/translation_list_binding.dart

@ -1,6 +1,7 @@
import 'package:get/get.dart';
import '../controllers/translation_list_controller.dart';
//翻译列表页面依赖绑定
class TranslationListBinding extends Bindings {
@override
void dependencies() {

3
lib/modules/translation/bindings/translation_stats_binding.dart

@ -2,6 +2,7 @@ import 'package:get/get.dart';
import '../controllers/translation_stats_controller.dart';
import '../../../data/services/translation_usage_service.dart';
//翻译统计页面依赖绑定
class TranslationStatsBinding implements Bindings {
@override
void dependencies() {
@ -9,7 +10,7 @@ class TranslationStatsBinding implements Bindings {
if (!Get.isRegistered<TranslationUsageService>()) {
Get.put(TranslationUsageService(), permanent: true);
}
// 注册控制器
Get.lazyPut<TranslationStatsController>(
() => TranslationStatsController(),

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

@ -31,7 +31,7 @@ enum AudioOutputDevice {
speaker, // 强制扬声器
}
/// 翻译控制器 - 负责管理语音识别、翻译和TTS功能(主控制器)
/// 翻译主控制器 - 负责管理语音识别、翻译和TTS功能(主控制器)
class TranslationController extends GetxController with WidgetsBindingObserver {
static const String TAG = 'TranslationController';
@ -62,6 +62,8 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
// 存储相关配置
static const String _sourceLanguageKey = 'translation_source_language';
static const String _targetLanguageKey = 'translation_target_language';
// 添加悬浮按钮位置存储键
static const String _floatingButtonPositionKey = 'floating_button_position';
// 分页加载相关状态
final pageSize = 20; // 每页记录数量
@ -77,6 +79,9 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
final isCreateRecord = false.obs;
final hasRecordPermission = false.obs;
// 添加悬浮按钮位置状态
final floatingButtonPosition = Rx<Offset>(Offset(2.0, 100.0)); // 默认位置
// ==================== 语言相关 ====================
final sourceLanguage = '中文(简体)'.obs;
final targetLanguage = '英语'.obs;
@ -319,6 +324,28 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
supportedLanguages.containsKey(savedTargetLanguage)) {
targetLanguage.value = savedTargetLanguage;
}
// 加载悬浮按钮位置
_loadFloatingButtonPosition();
}
/// 加载悬浮按钮位置
void _loadFloatingButtonPosition() {
final savedPosition = _storage.read(_floatingButtonPositionKey);
if (savedPosition != null && savedPosition is Map) {
final dx = savedPosition['dx']?.toDouble() ?? 2.0;
final dy = savedPosition['dy']?.toDouble() ?? 100.0;
floatingButtonPosition.value = Offset(dx, dy);
}
}
/// 保存悬浮按钮位置
void saveFloatingButtonPosition(Offset position) {
floatingButtonPosition.value = position;
_storage.write(_floatingButtonPositionKey, {
'dx': position.dx,
'dy': position.dy,
});
}
/// 保存当前语言设置
@ -686,6 +713,8 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
handleFinalResult(event.text);
} else {
faceToFaceRecognitionText.value += event.text;
// 添加日志打印
Logger.info('lxm-面对面翻译识别文本更新: ${faceToFaceRecognitionText.value}');
}
if (currentMode.value == 'faceToFace' && activeSpeaker.value == 0) {
handleFinalResult(faceToFaceRecognitionText.value);
@ -955,6 +984,20 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
translationHistory[index].targetLanguageCode = languageCodes['target']!;
translationHistory.refresh();
// 添加TranslationItem日志打印
if (isFinal) {
final item = translationHistory[index];
Logger.info('lxm-翻译完成 - TranslationItem详情: '
'源文本: ${item.sourceText}, '
'翻译文本: ${item.translatedText}, '
'源语言: ${item.sourceLanguageCode}, '
'目标语言: ${item.targetLanguageCode}, '
'时间戳: ${item.timestamp.toIso8601String()}, '
'会话ID: ${item.sessionId}, '
'是否会话首项: ${item.isFirstInSession}, '
'是否中间结果: ${item.isIntermediate}');
}
if (isFinal) {
saveTranslationHistory();
await _recordTranslationStats(

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

@ -12,7 +12,7 @@ class TranslationItem {
String sessionId;
final bool isFirstInSession;
bool isIntermediate; // 是否为中间结果
TranslationItem({
required this.sourceText,
required this.translatedText,
@ -23,7 +23,7 @@ class TranslationItem {
this.isFirstInSession = false,
this.isIntermediate = false,
});
/// 从JSON创建TranslationItem实例
factory TranslationItem.fromJson(Map<String, dynamic> json) {
DateTime timestamp;
@ -32,7 +32,7 @@ class TranslationItem {
} catch (e) {
timestamp = DateTime.now();
}
return TranslationItem(
sourceText: json['sourceText'] ?? '',
translatedText: json['translatedText'] ?? '',
@ -45,7 +45,7 @@ class TranslationItem {
isIntermediate: false, // 从存储加载的都是最终结果
);
}
/// 转换为JSON格式
Map<String, dynamic> toJson() {
return {
@ -59,7 +59,7 @@ class TranslationItem {
// 不保存isIntermediate,因为存储的都是最终结果
};
}
/// 获取源语言的中文名称
String get sourceLanguageName {
final languageManager = Get.find<LanguageManager>();
@ -73,4 +73,4 @@ class TranslationItem {
return languageManager.getChineseNameByAsrCode(targetLanguageCode) ??
'meetingUnknownLanguage'.tr; // 对应中文:未知语言
}
}
}

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

@ -20,13 +20,173 @@ class TranslationView extends GetView<TranslationController> {
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Obx(() {
// 面对面翻译模式使用特殊布局
if (controller.currentMode.value == 'faceToFace') {
return _buildFaceToFaceScaffold(isDarkMode);
}
// 其他模式使用原有布局
return Scaffold(
backgroundColor:
isDarkMode ? AppColors.darkBackground : Colors.grey[50],
appBar: _buildAppBar(isDarkMode),
body: Stack(
children: [
_buildBody(isDarkMode),
// 可拖拽的悬浮按钮
_buildDraggableFloatingButton(isDarkMode),
],
),
);
});
}
// 构建面对面翻译模式的专用Scaffold
Widget _buildFaceToFaceScaffold(bool isDarkMode) {
return Scaffold(
backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50],
appBar: _buildAppBar(isDarkMode),
body: _buildBody(isDarkMode),
body: Stack(
children: [
// 主要内容区域
_buildFaceToFaceLayout(isDarkMode),
// 顶部按钮(倒过来)
Positioned(
top: 50.h,
left: 0,
right: 0,
child: Transform.rotate(
angle: 3.14159, // 180度旋转
child: _buildFaceToFaceTopButton(isDarkMode),
),
),
// 底部按钮
Positioned(
bottom: 40.h,
left: 0,
right: 0,
child: _buildFaceToFaceBottomButton(isDarkMode),
),
// 可拖拽的悬浮按钮 - 在所有模式下都显示
_buildDraggableFloatingButton(isDarkMode),
],
),
);
}
// 构建面对面翻译顶部按钮(原右边按钮,现在移到顶部并旋转)
Widget _buildFaceToFaceTopButton(bool isDarkMode) {
return Center(
child: Transform.rotate(
angle: 0, // 180度旋转
child: _buildHoldToTalkButton(
isDarkMode: isDarkMode,
speakerId: 2, // 说话者2(原右边按钮)
activeSpeaker: controller.activeSpeaker.value,
icon: Icons.mic_none,
label: controller.targetLanguage.value.split(' ')[0].tr, // 目标语言名称
),
),
);
}
// 构建面对面翻译底部按钮(原左边按钮,保留在底部)
Widget _buildFaceToFaceBottomButton(bool isDarkMode) {
return Center(
child: _buildHoldToTalkButton(
isDarkMode: isDarkMode,
speakerId: 1, // 说话者1(原左边按钮)
activeSpeaker: controller.activeSpeaker.value,
icon: Icons.mic_none,
label: controller.sourceLanguage.value.split(' ')[0].tr, // 源语言名称
),
);
}
// 构建可拖拽的悬浮按钮
Widget _buildDraggableFloatingButton(bool isDarkMode) {
return Obx(() => Positioned(
right: controller.floatingButtonPosition.value.dx, // 使用保存的位置
bottom: controller.floatingButtonPosition.value.dy, // 使用保存的位置
child: Draggable(
feedback: Material(
color: Colors.transparent,
child: Container(
width: 56.w,
height: 56.w,
decoration: BoxDecoration(
color: (isDarkMode ? Colors.grey[700] : Colors.blue)
?.withOpacity(0.8),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Icon(
Icons.translate,
color: Colors.white,
size: 24.sp,
),
),
),
childWhenDragging: Container(
width: 56.w,
height: 56.w,
decoration: BoxDecoration(
color: (isDarkMode ? Colors.grey[700] : Colors.blue)
?.withOpacity(0.3),
shape: BoxShape.circle,
),
),
onDragEnd: (details) {
// 计算新位置(相对于屏幕右下角)
final screenSize = MediaQuery.of(Get.context!).size;
final newRight = screenSize.width - details.offset.dx - 56.w;
final newBottom = screenSize.height - details.offset.dy - 56.w;
// 边界检查,确保按钮不会超出屏幕
final clampedRight = newRight.clamp(0.0, screenSize.width - 56.w);
final clampedBottom =
newBottom.clamp(0.0, screenSize.height - 56.w);
// 保存新位置
controller.saveFloatingButtonPosition(
Offset(clampedRight, clampedBottom));
},
child: GestureDetector(
onTap: () => _showTranslationModeSelector(isDarkMode),
child: Container(
width: 56.w,
height: 56.w,
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[700] : Colors.blue,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
),
child: Icon(
Icons.translate,
color: Colors.white,
size: 24.sp,
),
),
),
),
));
}
PreferredSizeWidget _buildAppBar(bool isDarkMode) {
return AppBar(
backgroundColor: isDarkMode ? AppColors.cardBackground : Colors.white,
@ -260,6 +420,7 @@ class TranslationView extends GetView<TranslationController> {
Widget _buildTranslationContent(bool isDarkMode) {
return Obx(() {
// 其他模式使用原有的列表布局
final itemCount = controller.translationHistory.length +
(controller.hasMoreHistory.value ? 1 : 0);
@ -299,6 +460,99 @@ class TranslationView extends GetView<TranslationController> {
});
}
/// 构建面对面翻译的对称布局
Widget _buildFaceToFaceLayout(bool isDarkMode) {
return Obx(() {
// 获取最新的翻译记录
final latestItem = controller.translationHistory.isNotEmpty
? controller.translationHistory.last
: null;
return Column(
children: [
// 上半部分 - (旋转180度)
Expanded(
child: Transform.rotate(
angle: 3.14159, // 180度 = π弧度
child: Container(
width: double.infinity,
padding: EdgeInsets.all(24.w),
decoration: BoxDecoration(
color: isDarkMode
? AppColors.cardBackground.withValues(alpha: 0.5)
: Colors.blue[50]?.withValues(alpha: 0.3),
border: Border(
bottom: BorderSide(
color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!,
width: 1,
),
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 上半部的文本
Text(
latestItem?.targetLanguageCode ==
controller.targetLanguageCode.value
? latestItem?.translatedText ?? '' //如果为null则显示空字符
: latestItem?.sourceText ?? '',
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black87,
height: 1.4,
),
textAlign: TextAlign.center,
),
],
),
),
),
),
// 语言选择栏
_buildLanguageSelectionBar(isDarkMode),
// 下半部分 - (正常方向)
Expanded(
child: Container(
width: double.infinity,
padding: EdgeInsets.all(24.w),
decoration: BoxDecoration(
color: isDarkMode
? AppColors.cardBackground.withValues(alpha: 0.5)
: Colors.green[50]?.withValues(alpha: 0.3),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 下半部的文本
Text(
latestItem?.sourceLanguageCode ==
controller.sourceLanguageCode.value
? latestItem?.sourceText ??
(controller.faceToFaceRecognitionText.value.isEmpty
? ''
: controller.faceToFaceRecognitionText.value)
: latestItem?.translatedText ?? '',
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black87,
height: 1.4,
),
textAlign: TextAlign.center,
),
],
),
),
),
],
);
});
}
Widget _buildTranslationItemWidget(
BuildContext context, TranslationItem item) {
return Container(
@ -1201,4 +1455,106 @@ class TranslationView extends GetView<TranslationController> {
),
);
}
// 构建语言选择栏
Widget _buildLanguageSelectionBar(bool isDarkMode) {
return Container(
height: 44.h,
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 6.h),
decoration: BoxDecoration(
color: isDarkMode ? AppColors.cardBackground : Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, 2),
),
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, -2),
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 源语言选择
Expanded(
child: GestureDetector(
onTap: () => _showLanguageSelectionDialog(true, isDarkMode),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[100],
borderRadius: BorderRadius.circular(16.r),
),
child: Text(
'${controller.sourceLanguage.value.split(' ')[0]}'.tr,
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
),
),
),
// 语言互换按钮
GestureDetector(
onTap: controller.swapLanguages,
child: Container(
margin: EdgeInsets.symmetric(horizontal: 8.w),
padding: EdgeInsets.all(4.r),
decoration: BoxDecoration(
color: isDarkMode
? AppColors.primary.withOpacity(0.2)
: Colors.blue[50],
shape: BoxShape.circle,
),
child: Icon(
Icons.swap_horiz,
color: isDarkMode ? AppColors.primary : Colors.blue,
size: 20.sp,
),
),
),
// 目标语言选择
Expanded(
child: GestureDetector(
onTap: () => _showLanguageSelectionDialog(false, isDarkMode),
child: Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[100],
borderRadius: BorderRadius.circular(16.r),
),
child: Transform.rotate(
angle: 3.14159, // 旋转180度
child: Text(
'${controller.targetLanguage.value.split(' ')[0]}'.tr,
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
),
),
),
),
],
),
);
}
}

Loading…
Cancel
Save