5 changed files with 680 additions and 491 deletions
@ -0,0 +1,161 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|||
import '../../controllers/translation_controller.dart'; |
|||
|
|||
/// 语言选择对话框 |
|||
class LanguageSelectionDialog { |
|||
/// 显示语言选择对话框 |
|||
static void show(bool isSource, bool isDarkMode) { |
|||
final controller = Get.find<TranslationController>(); |
|||
|
|||
Get.dialog(Dialog( |
|||
backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(16.r), // 统一圆角尺寸为16 |
|||
), |
|||
alignment: isSource ? Alignment.centerLeft : Alignment.centerRight, |
|||
insetPadding: EdgeInsets.symmetric(horizontal: 10.w), |
|||
child: SizedBox( |
|||
width: 0.4.sw, |
|||
child: Padding( |
|||
// 使用Padding替代Container的padding |
|||
padding: EdgeInsets.all(12.w), // 统一内边距 |
|||
child: Column( |
|||
mainAxisSize: MainAxisSize.min, |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
// 标题区域 |
|||
Padding( |
|||
padding: EdgeInsets.symmetric(horizontal: 8.w), |
|||
child: Text( |
|||
isSource |
|||
? 'selectSourceLanguage'.tr |
|||
: 'selectTargetLanguage'.tr, // 保留源语言/目标语言区分 |
|||
style: TextStyle( |
|||
fontSize: 18.sp, // 调整字号 |
|||
fontWeight: FontWeight.w600, // 使用数字字重 |
|||
color: isDarkMode ? Colors.white : Colors.black87, // 优化文本颜色 |
|||
), |
|||
), |
|||
), |
|||
SizedBox(height: 16.h), |
|||
|
|||
// 语言列表区域 |
|||
Container( |
|||
constraints: BoxConstraints(maxHeight: 0.5.sh), |
|||
child: SingleChildScrollView( |
|||
child: Column( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: |
|||
controller.supportedLanguages.keys.map((language) { |
|||
return Obx(() { |
|||
final currentLanguage = isSource |
|||
? controller.sourceLanguage.value |
|||
: controller.targetLanguage.value; |
|||
final isSelected = currentLanguage == language; |
|||
|
|||
return GestureDetector( |
|||
// 改用手势检测器实现点击 |
|||
onTap: () { |
|||
if (isSource) { |
|||
controller.setSourceLanguage(language); |
|||
} else { |
|||
controller.setTargetLanguage(language); |
|||
} |
|||
Get.back(); |
|||
}, |
|||
child: Container( |
|||
margin: EdgeInsets.only(bottom: 10.h), // 添加项间距 |
|||
padding: EdgeInsets.all(10.w), // 统一内边距 |
|||
decoration: BoxDecoration( |
|||
color: isSelected // 选中状态背景色 |
|||
? (isDarkMode |
|||
? const Color(0xFF6200EE).withOpacity(0.2) |
|||
: Colors.blue.withOpacity(0.1)) |
|||
: (isDarkMode // 未选中状态背景色 |
|||
? Colors.white.withOpacity(0.05) |
|||
: Colors.grey[200]!), |
|||
borderRadius: BorderRadius.circular(12.r), // 项圆角 |
|||
border: Border.all( |
|||
// 选中状态边框 |
|||
color: isSelected |
|||
? (isDarkMode |
|||
? const Color(0xFF6200EE) |
|||
: Colors.blue) |
|||
: (isDarkMode // 未选中状态背景色 |
|||
? Colors.white.withOpacity(0.05) |
|||
: Colors.grey[200]!), |
|||
width: 1, |
|||
), |
|||
), |
|||
child: Row( |
|||
children: [ |
|||
// 语言名称 |
|||
Expanded( |
|||
child: Text( |
|||
language.tr, |
|||
style: TextStyle( |
|||
fontSize: 16.sp, |
|||
fontWeight: FontWeight.w500, |
|||
color: isDarkMode |
|||
? Colors.white |
|||
: Colors.black87, |
|||
overflow: TextOverflow.ellipsis, |
|||
), |
|||
), |
|||
), |
|||
// 选中指示器 |
|||
Container( |
|||
width: 24.w, |
|||
height: 24.w, |
|||
decoration: BoxDecoration( |
|||
color: isSelected // 动态颜色 |
|||
? (isDarkMode |
|||
? const Color(0xFF6200EE) |
|||
: Colors.blue) |
|||
: (isDarkMode |
|||
? Colors.white.withOpacity(0.1) |
|||
: Colors.grey[300]), |
|||
shape: BoxShape.circle, // 圆形样式 |
|||
), |
|||
child: isSelected |
|||
? Icon( |
|||
// 选中时显示对勾 |
|||
Icons.check, |
|||
size: 16.w, |
|||
color: Colors.white, |
|||
) |
|||
: null, |
|||
), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
}); |
|||
}).toList(), |
|||
), |
|||
), |
|||
), |
|||
SizedBox(height: 16.h), |
|||
|
|||
// 取消按钮 |
|||
Align( |
|||
alignment: Alignment.centerRight, |
|||
child: TextButton( |
|||
onPressed: () => Get.back(), |
|||
child: Text( |
|||
'cancel'.tr, |
|||
style: TextStyle( |
|||
color: isDarkMode ? const Color(0xFF6200EE) : Colors.blue, |
|||
), |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
), |
|||
)); |
|||
} |
|||
} |
|||
@ -0,0 +1,172 @@ |
|||
import 'dart:ui'; |
|||
import 'dart:io'; |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|||
import '../../controllers/translation_controller.dart'; |
|||
|
|||
/// 翻译模式选择对话框 |
|||
class TranslationModeSelector { |
|||
/// 显示翻译模式选择器 |
|||
static void show(bool isDarkMode) { |
|||
final controller = Get.find<TranslationController>(); |
|||
|
|||
Get.bottomSheet( |
|||
BackdropFilter( |
|||
filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), |
|||
child: _buildTranslationModeSelector(controller), |
|||
), |
|||
isScrollControlled: true, |
|||
backgroundColor: Colors.transparent, |
|||
); |
|||
} |
|||
|
|||
// 构建翻译模式选择器 - 参考会议模块样式 |
|||
static Widget _buildTranslationModeSelector(TranslationController controller) { |
|||
return Builder( |
|||
builder: (BuildContext context) { |
|||
return SafeArea( |
|||
child: Column( |
|||
mainAxisSize: MainAxisSize.min, |
|||
children: [ |
|||
_buildModeItem( |
|||
context, |
|||
'simultaneousTranslation'.tr, |
|||
Icons.translate, |
|||
Colors.blue, |
|||
controller, |
|||
onTap: () async { |
|||
Get.back(); |
|||
await controller.stopAll(); |
|||
await controller.changeTranslationMode('simultaneous'); |
|||
await controller.startRecognition(); |
|||
}, |
|||
), |
|||
_buildModeItem( |
|||
context, |
|||
'faceToFaceTranslation'.tr, |
|||
Icons.people, |
|||
Colors.green, |
|||
controller, |
|||
onTap: () async { |
|||
Get.back(); |
|||
await controller.stopAll(); |
|||
await controller.changeTranslationMode('faceToFace'); |
|||
await controller.startRecognition(); |
|||
}, |
|||
), |
|||
_buildModeItem( |
|||
context, |
|||
'audioVideoTranslation'.tr, |
|||
Icons.videocam, |
|||
Colors.orange, |
|||
controller, |
|||
onTap: controller.bleManager.isConnected |
|||
? () async { |
|||
Get.back(); |
|||
await controller.stopAll(); |
|||
await controller.changeTranslationMode('audioVideo'); |
|||
await controller.startRecognition(); |
|||
} |
|||
: null, |
|||
), |
|||
_buildModeItem( |
|||
context, |
|||
'callTranslation'.tr, |
|||
Icons.phone, |
|||
Colors.purple, |
|||
controller, |
|||
onTap: controller.bleManager.isConnected |
|||
? () async { |
|||
Get.back(); |
|||
await controller.stopAll(); |
|||
await controller.changeTranslationMode('call'); |
|||
await controller.startRecognition(); |
|||
} |
|||
: null, |
|||
), |
|||
SizedBox(height: 24.w), |
|||
GestureDetector( |
|||
onTap: () => Get.back(), |
|||
child: Container( |
|||
width: 42, |
|||
height: 42, |
|||
decoration: BoxDecoration( |
|||
color: Colors.black.withValues(alpha: 0.8), |
|||
borderRadius: BorderRadius.circular(50), |
|||
), |
|||
child: const Icon(Icons.close, color: Colors.white), |
|||
), |
|||
), |
|||
if (Platform.isAndroid) SizedBox(height: 20.h), |
|||
], |
|||
), |
|||
); |
|||
}, |
|||
); |
|||
} |
|||
|
|||
// 构建单个模式选项 - 完全参考会议模块的 _buildItem 方法 |
|||
static Widget _buildModeItem( |
|||
BuildContext context, |
|||
String title, |
|||
IconData icon, |
|||
Color color, |
|||
TranslationController controller, { |
|||
Function()? onTap, |
|||
}) { |
|||
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
|||
|
|||
return GestureDetector( |
|||
onTap: onTap, |
|||
child: Container( |
|||
width: 220.w, |
|||
height: 60.w, |
|||
padding: EdgeInsets.all(5.w), |
|||
margin: EdgeInsets.only(top: 15.w), |
|||
decoration: BoxDecoration( |
|||
color: isDarkMode |
|||
? Colors.grey[800]!.withValues(alpha: onTap != null ? 1 : 0.5) |
|||
: Colors.white.withValues(alpha: onTap != null ? 1 : 0.5), |
|||
borderRadius: BorderRadius.circular(60.w), |
|||
), |
|||
child: Row( |
|||
children: [ |
|||
Container( |
|||
width: 46.w, |
|||
height: 46.w, |
|||
margin: EdgeInsets.only(right: 5.w), |
|||
decoration: BoxDecoration( |
|||
color: color.withValues(alpha: onTap != null ? 0.3 : 0.2), |
|||
borderRadius: BorderRadius.circular(60.w), |
|||
), |
|||
child: Icon( |
|||
icon, |
|||
color: color.withValues(alpha: onTap != null ? 1 : 0.5), |
|||
), |
|||
), |
|||
Expanded( |
|||
child: Text( |
|||
title, |
|||
style: TextStyle( |
|||
fontSize: 15.sp, |
|||
color: isDarkMode |
|||
? Colors.white.withValues(alpha: onTap != null ? 1 : 0.5) |
|||
: Colors.black87 |
|||
.withValues(alpha: onTap != null ? 1 : 0.5), |
|||
fontWeight: FontWeight.bold, |
|||
), |
|||
), |
|||
), |
|||
Icon( |
|||
Icons.arrow_forward_ios, |
|||
size: 16.w, |
|||
color: Colors.grey.withValues(alpha: onTap != null ? 1 : 0.5), |
|||
), |
|||
SizedBox(width: 5.w), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,103 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|||
import '../../controllers/translation_controller.dart'; |
|||
import '../dialogs/translation_mode_selector.dart'; |
|||
|
|||
/// 可拖拽悬浮按钮组件 |
|||
class DraggableFloatingButton extends StatelessWidget { |
|||
final bool isDarkMode; |
|||
final TranslationController controller; |
|||
|
|||
const DraggableFloatingButton({ |
|||
Key? key, |
|||
required this.isDarkMode, |
|||
required this.controller, |
|||
}) : super(key: key); |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
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, |
|||
), |
|||
), |
|||
), |
|||
), |
|||
)); |
|||
} |
|||
|
|||
/// 显示翻译模式选择器 |
|||
void _showTranslationModeSelector(bool isDarkMode) { |
|||
TranslationModeSelector.show(isDarkMode); |
|||
} |
|||
} |
|||
@ -0,0 +1,219 @@ |
|||
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 '../../../../core/theme/app_colors.dart'; |
|||
import '../dialogs/language_selection_dialog.dart'; |
|||
|
|||
/// 面对面翻译布局组件 |
|||
class FaceToFaceLayout extends StatelessWidget { |
|||
final bool isDarkMode; |
|||
final TranslationController controller; |
|||
|
|||
const FaceToFaceLayout({ |
|||
Key? key, |
|||
required this.isDarkMode, |
|||
required this.controller, |
|||
}) : super(key: key); |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
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 _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, |
|||
), |
|||
), |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
/// 显示语言选择对话框 |
|||
void _showLanguageSelectionDialog(bool isSource, bool isDarkMode) { |
|||
LanguageSelectionDialog.show(isSource, isDarkMode); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue