diff --git a/lib/data/models/appconfig.dart b/lib/data/models/appconfig.dart index 1d1b844ab..fa5cb0922 100644 --- a/lib/data/models/appconfig.dart +++ b/lib/data/models/appconfig.dart @@ -47,6 +47,7 @@ class AppConfig { return uuids; } +//根据产品名称获取产品配置 static DBProduct? getproduct(String name) { final foundProduct = config.products.firstWhere((product) => product.devicename == name); diff --git a/lib/data/models/appconfig_model.dart b/lib/data/models/appconfig_model.dart index b4b6a4f90..945103b42 100644 --- a/lib/data/models/appconfig_model.dart +++ b/lib/data/models/appconfig_model.dart @@ -75,6 +75,7 @@ class DBProduct { final int factoryid; final int devicetype; final String devicename; + final String? productimage; final String scanuuid; final String? version; final String? updatedescription; @@ -85,6 +86,7 @@ class DBProduct { required this.factoryid, required this.devicetype, required this.devicename, + required this.productimage, required this.scanuuid, required this.version, required this.updatedescription, diff --git a/lib/data/models/appconfig_model.g.dart b/lib/data/models/appconfig_model.g.dart index b8e08075e..e75d17437 100644 --- a/lib/data/models/appconfig_model.g.dart +++ b/lib/data/models/appconfig_model.g.dart @@ -76,6 +76,7 @@ DBProduct _$DBProductFromJson(Map json) => DBProduct( factoryid: (json['factoryid'] as num).toInt(), devicename: json['devicename'] as String, devicetype: json['devicetype'] as int, + productimage: json['productimage'] as String?, scanuuid: json['scanuuid'] as String, version: json['version'] as String?, updatedescription: json['updatedescription'] as String?, diff --git a/lib/modules/home/controllers/home_controller.dart b/lib/modules/home/controllers/home_controller.dart index 8b47ae480..c5cb174cb 100644 --- a/lib/modules/home/controllers/home_controller.dart +++ b/lib/modules/home/controllers/home_controller.dart @@ -22,8 +22,12 @@ enum ConnectionStatus { /// 主页控制器,管理设备连接状态和信息展示 class HomeController extends GetxController { static const String _tag = 'HomeCtrl'; + static const String _lastDeviceNameKey = 'last_connected_device_name'; // 存储键 + static const String _lastDeviceImageKey = + 'last_connected_device_image'; // 存储上一次设备图片的键 static HomeController get to => Get.find(); final GetStorage _storage = GetStorage(); + String? _lastConnectedDeviceImage; // 新增:存储上一次连接设备的图片链接 final _isFullyConnected = false.obs; String? _lastConnectedDeviceName; @@ -98,6 +102,7 @@ class HomeController extends GetxController { void onInit() { super.onInit(); _otaCheckService = Get.find(); + _loadLastConnectedDeviceName(); // 加载持久化的设备名称 _initWithCachedInfo(); // 移除了电池信息定时刷新的启动 } @@ -132,7 +137,7 @@ class HomeController extends GetxController { _classicBluetoothWorker = ever(_bluetoothManager.currentDeviceRx, (BluetoothDevice? device) { if (device != null) { - _lastConnectedDeviceName = device.name; + _saveLastConnectedDeviceName(device.name); // 使用新的保存方法 } _checkConnectionStatus(); }); @@ -142,6 +147,28 @@ class HomeController extends GetxController { }); } + /// 从本地存储加载最后连接的设备名称 + void _loadLastConnectedDeviceName() { + _lastConnectedDeviceName = _storage.read(_lastDeviceNameKey); + Logger.d(_tag, '加载最后连接的设备名称: $_lastConnectedDeviceName'); + } + + /// 保存最后连接的设备名称到本地存储 + void _saveLastConnectedDeviceName(String deviceName) { + _lastConnectedDeviceName = deviceName; + _storage.write(_lastDeviceNameKey, deviceName); + Logger.d(_tag, '保存最后连接的设备名称: $deviceName'); + } + + /// 保存上一次连接设备的图片链接到本地存储 + void _saveLastConnectedDeviceImage(String? imageUrl) { + if (imageUrl != null && imageUrl.isNotEmpty) { + _lastConnectedDeviceImage = imageUrl; + _storage.write(_lastDeviceImageKey, imageUrl); + Logger.d(_tag, '保存上一次连接设备的图片: $imageUrl'); + } + } + Future refreshDeviceConnection() async { await _checkConnectionStatus(); @@ -235,6 +262,7 @@ class HomeController extends GetxController { } final deviceName = currentDevice?.name; + //final deviceName = '102'; if (deviceName != null && deviceName.isNotEmpty) { return deviceName; } @@ -404,4 +432,69 @@ class HomeController extends GetxController { Map getOtaCheckStatus() { return _otaCheckService.getCheckStatus(); } + + /// 获取当前设备的产品图片路径 + /// 如果没有配置产品图片或获取失败,返回null使用默认图片 + String? getCurrentDeviceImage() { + try { + final deviceName = getDeviceName(); + if (deviceName == 'unknownEquipment'.tr) { + Logger.d(_tag, '设备未知,使用默认图片'); + return null; + } + + // 通过AppConfig获取产品信息 + if (!AppConfig.isInitialized()) { + Logger.w(_tag, 'AppConfig未初始化,使用默认图片'); + return null; + } + + final product = AppConfig.getproduct(deviceName); + if (product != null && + product.productimage != null && + product.productimage!.isNotEmpty) { + Logger.d(_tag, '获取到设备产品图片: ${product.productimage}'); + _saveLastConnectedDeviceImage(product.productimage); + return product.productimage; + } else { + Logger.d(_tag, '设备 "$deviceName" 未配置产品图片,使用默认图片'); + return null; + } + } catch (e) { + Logger.e(_tag, '获取设备产品图片失败: $e'); + return null; + } + } + + /// 获取设备图片的完整资源路径 + /// 处理网络图片URL和本地资源路径 + String getDeviceImagePath(bool isDarkMode) { + final productImage = getCurrentDeviceImage(); + + if (productImage != null) { + // 如果是网络图片URL + if (productImage.startsWith('http://') || + productImage.startsWith('https://')) { + return productImage; + } + // 如果是本地资源路径,确保以assets/开头 + else if (productImage.startsWith('assets/')) { + return productImage; + } + // 如果只是文件名,添加assets/images/前缀 + else { + return 'assets/images/$productImage'; + } + } + + // 使用默认耳机图片 + return isDarkMode + ? 'assets/images/headphone_new3.png' + : 'assets/images/headphone_new4.png'; + } + + /// 判断是否为网络图片 + bool isNetworkImage(String imagePath) { + return imagePath.startsWith('http://') || imagePath.startsWith('https://'); + } } diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart index 4541a14d3..51cdbb387 100644 --- a/lib/modules/home/views/home_view.dart +++ b/lib/modules/home/views/home_view.dart @@ -220,12 +220,58 @@ class HomeView extends GetView { child: ClipRRect( // 添加 ClipRRect borderRadius: BorderRadius.circular(16.r), - child: Image.asset( - isDarkMode - ? 'assets/images/headphone_new3.png' - : 'assets/images/headphone_new4.png', - height: 220.h, - fit: BoxFit.contain, + child: GetBuilder( + builder: (controller) { + final imagePath = controller.getDeviceImagePath(isDarkMode); + final isNetwork = controller.isNetworkImage(imagePath); + + return isNetwork + ? Image.network( + imagePath, + height: 220.h, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + // 网络图片加载失败时使用默认图片 + return Image.asset( + isDarkMode + ? 'assets/images/headphone_new3.png' + : 'assets/images/headphone_new4.png', + height: 220.h, + fit: BoxFit.contain, + ); + }, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return SizedBox( + height: 220.h, + child: Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != + null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ), + ); + }, + ) + : Image.asset( + imagePath, + height: 220.h, + fit: BoxFit.contain, + errorBuilder: (context, error, stackTrace) { + // 本地图片加载失败时使用默认图片 + return Image.asset( + isDarkMode + ? 'assets/images/headphone_new3.png' + : 'assets/images/headphone_new4.png', + height: 220.h, + fit: BoxFit.contain, + ); + }, + ); + }, ), ), ), diff --git a/lib/modules/login/controllers/login_controller.dart b/lib/modules/login/controllers/login_controller.dart index 6462b66f1..a159b0269 100644 --- a/lib/modules/login/controllers/login_controller.dart +++ b/lib/modules/login/controllers/login_controller.dart @@ -932,7 +932,59 @@ class LoginController extends GetxController { Logger().d('getAppConfig:进来了'); // 根据API设计调整 final resp = UserGetAppConfigResp.fromJson(response); + + // 详细记录解析后的配置信息 + Logger().d('getAppConfig: 配置解析成功'); + Logger().d('getAppConfig: 环境变量数量 = ${resp.env.length}'); + Logger().d('getAppConfig: 智能体数量 = ${resp.agents.length}'); + Logger().d('getAppConfig: MCP服务器数量 = ${resp.mcps.length}'); + Logger().d('getAppConfig: 产品设备数量 = ${resp.products.length}'); + + // 详细记录产品设备信息 + if (resp.products.isNotEmpty) { + Logger().d('getAppConfig: ========== 产品设备详细信息 =========='); + for (int i = 0; i < resp.products.length; i++) { + final product = resp.products[i]; + Logger().d( + 'getAppConfig: 产品[$i] ID=${product.id}, 设备名称="${product.devicename}"'); + Logger().d( + 'getAppConfig: 产品[$i] 厂商ID=${product.factoryid}, 设备类型=${product.devicetype}'); + Logger().d('getAppConfig: 产品[$i] 扫描UUID="${product.scanuuid}"'); + Logger().d( + 'getAppConfig: 产品[$i] 产品图片="${product.productimage ?? "未设置"}"'); + Logger().d('getAppConfig: 产品[$i] 版本="${product.version ?? "未设置"}"'); + Logger().d( + 'getAppConfig: 产品[$i] 更新描述="${product.updatedescription ?? "未设置"}"'); + Logger().d( + 'getAppConfig: 产品[$i] 更新包地址="${product.updatepackageaddress ?? "未设置"}"'); + Logger().d('getAppConfig: ----------------------------------------'); + } + Logger().d('getAppConfig: ========== 产品设备信息结束 =========='); + } else { + Logger().w('getAppConfig: 警告 - 未获取到任何产品设备信息'); + } + + // 记录环境变量信息 + if (resp.env.isNotEmpty) { + Logger().d('getAppConfig: ========== 环境变量信息 =========='); + resp.env.forEach((key, value) { + Logger().d('getAppConfig: 环境变量 $key = "$value"'); + }); + Logger().d('getAppConfig: ========== 环境变量信息结束 =========='); + } + AppConfig.initialize(resp); + Logger().d( + 'getAppConfig: AppConfig初始化完成,产品数量验证 = ${AppConfig.config.products.length}'); + + // 验证AppConfig中的产品数据 + final productNames = AppConfig.allproductNames(); + final productUuids = AppConfig.allproductUuids(); + Logger().d('getAppConfig: 可用产品名称列表 = $productNames'); + Logger().d('getAppConfig: 可用产品UUID列表 = $productUuids'); + + Logger().d('getAppConfig: 应用配置获取和初始化成功完成'); + return true; } catch (e) { Logger().d('getAppConfig 取失败: $e'); diff --git a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart index f306bde15..4a4e9e896 100644 --- a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart +++ b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart @@ -1,12 +1,14 @@ import 'package:classic_bluetooth/classic_bluetooth.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:logger/web.dart'; +// import 'package:logger/web.dart'; +import 'package:get_storage/get_storage.dart'; import 'package:ota/ota.dart'; import '../../../data/services/ble_manager.dart'; import 'ota_core_contropller.dart'; import 'ota_download_contropller.dart'; import '../views/ota_upgrade_popup_view.dart'; +import '../../../core/utils/logger.dart'; /// OTA升级主控制器 - 重构后的简化版本 /// 负责协调各个服务,管理升级流程的整体状态 @@ -33,13 +35,18 @@ class OtaUpgradeController extends GetxController { final RxBool isFetchingDeviceInfo = false.obs; final RxBool showDetailedStatus = false.obs; final RxBool isBleConnected = true.obs; + final GetStorage _storage = GetStorage(); String _address = ''; String _updatePackageAddress = ''; + String? _lastConnectedDeviceImage; // 新增:存储上一次连接设备的图片链接 + + get lastConnectedDeviceImage => _lastConnectedDeviceImage; @override void onInit() { super.onInit(); + _loadLastConnectedDeviceImage(); // 加载持久化的设备图片 _initializeController(); } @@ -51,6 +58,13 @@ class OtaUpgradeController extends GetxController { super.onClose(); } + /// 从本地存储加载上一次连接设备的图片链接 + void _loadLastConnectedDeviceImage() { + _lastConnectedDeviceImage = _storage.read('last_connected_device_name'); + Logger.d( + 'OtaUpgradeController', '加载上一次连接设备的图片: $_lastConnectedDeviceImage'); + } + /// 初始化控制器 /// 设置事件监听并加载版本信息 Future _initializeController() async { @@ -169,7 +183,7 @@ class OtaUpgradeController extends GetxController { detailedStatus.value = '下载失败,请检查网络连接'; showDetailedStatus.value = false; _handleError('下载失败', e.toString()); - Logger().d('下载失败: $e'); + Logger.d('OtaUpgradeController', '下载失败: $e'); } } diff --git a/lib/modules/ota_upgrade/views/ota_upgrade_view.dart b/lib/modules/ota_upgrade/views/ota_upgrade_view.dart index e9011531a..10f7180b8 100644 --- a/lib/modules/ota_upgrade/views/ota_upgrade_view.dart +++ b/lib/modules/ota_upgrade/views/ota_upgrade_view.dart @@ -185,12 +185,43 @@ class OtaUpgradeView extends GetView { width: 1, ), ), - child: Image.asset( - isDarkMode - ? 'assets/images/headphone_new3.png' - : 'assets/images/headphone_new4.png', - height: 100.h, - fit: BoxFit.contain, + child: GetBuilder( + builder: (controller) { + final imagePath = controller.lastConnectedDeviceImage; + + // 网络图片 + return Image.network( + imagePath, + height: 100.h, + fit: BoxFit.contain, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return SizedBox( + height: 100.h, + child: Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + strokeWidth: 2, + color: isDarkMode ? Colors.white : Colors.blue, + ), + ), + ); + }, + errorBuilder: (context, error, stackTrace) { + // 网络图片加载失败时使用默认图片 + return Image.asset( + isDarkMode + ? 'assets/images/headphone_new3.png' + : 'assets/images/headphone_new4.png', + height: 100.h, + fit: BoxFit.contain, + ); + }, + ); + }, ), ); } diff --git a/lib/modules/translation/bindings/translation_binding.dart b/lib/modules/translation/bindings/translation_binding.dart index b9071e619..afc89fc87 100644 --- a/lib/modules/translation/bindings/translation_binding.dart +++ b/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()); } -} \ No newline at end of file +} diff --git a/lib/modules/translation/bindings/translation_detail_binding.dart b/lib/modules/translation/bindings/translation_detail_binding.dart index 5e779995d..5b50d238f 100644 --- a/lib/modules/translation/bindings/translation_detail_binding.dart +++ b/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()); + Get.lazyPut( + () => TranslationDetailController()); } -} \ No newline at end of file +} diff --git a/lib/modules/translation/bindings/translation_history_binding.dart b/lib/modules/translation/bindings/translation_history_binding.dart index 613b525f6..8b63d17c0 100644 --- a/lib/modules/translation/bindings/translation_history_binding.dart +++ b/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() { diff --git a/lib/modules/translation/bindings/translation_list_binding.dart b/lib/modules/translation/bindings/translation_list_binding.dart index 4dc264ada..9d753c170 100644 --- a/lib/modules/translation/bindings/translation_list_binding.dart +++ b/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() { diff --git a/lib/modules/translation/bindings/translation_stats_binding.dart b/lib/modules/translation/bindings/translation_stats_binding.dart index 031cdc0ea..45c2824b1 100644 --- a/lib/modules/translation/bindings/translation_stats_binding.dart +++ b/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()) { Get.put(TranslationUsageService(), permanent: true); } - + // 注册控制器 Get.lazyPut( () => TranslationStatsController(), diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index f1bfff331..cfb187c98 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/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(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( diff --git a/lib/modules/translation/models/translation_models.dart b/lib/modules/translation/models/translation_models.dart index 4f9679539..d4df1913d 100644 --- a/lib/modules/translation/models/translation_models.dart +++ b/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 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 toJson() { return { @@ -59,7 +59,7 @@ class TranslationItem { // 不保存isIntermediate,因为存储的都是最终结果 }; } - + /// 获取源语言的中文名称 String get sourceLanguageName { final languageManager = Get.find(); @@ -73,4 +73,4 @@ class TranslationItem { return languageManager.getChineseNameByAsrCode(targetLanguageCode) ?? 'meetingUnknownLanguage'.tr; // 对应中文:未知语言 } -} \ No newline at end of file +} diff --git a/lib/modules/translation/views/dialogs/language_selection_dialog.dart b/lib/modules/translation/views/dialogs/language_selection_dialog.dart new file mode 100644 index 000000000..cbd889f5c --- /dev/null +++ b/lib/modules/translation/views/dialogs/language_selection_dialog.dart @@ -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(); + + 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, + ), + ), + ), + ), + ], + ), + ), + ), + )); + } +} diff --git a/lib/modules/translation/views/dialogs/translation_mode_selector.dart b/lib/modules/translation/views/dialogs/translation_mode_selector.dart new file mode 100644 index 000000000..75386980c --- /dev/null +++ b/lib/modules/translation/views/dialogs/translation_mode_selector.dart @@ -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(); + + 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), + ], + ), + ), + ); + } +} diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index b1f91a87c..69abbd234 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -10,6 +10,13 @@ import '../views/translation_history_view.dart'; import '../bindings/translation_history_binding.dart'; import '../../../core/theme/app_colors.dart'; import '../../../routes/app_routes.dart'; + +// 导入拆分后的组件 +import 'widgets/face_to_face_layout.dart'; // 面对面翻译布局 +import 'widgets/draggable_floating_button.dart'; // 可拖拽的悬浮按钮 +import 'dialogs/language_selection_dialog.dart'; // 语言选择对话框 +import 'dialogs/translation_mode_selector.dart'; // 翻译模式选择器 + // import 'package:intl/intl.dart'; //主页面 @@ -20,10 +27,98 @@ class TranslationView extends GetView { 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), + // 可拖拽的悬浮按钮 + DraggableFloatingButton( + isDarkMode: isDarkMode, + controller: controller, + ), + ], + ), + ); + }); + } + + // 构建面对面翻译模式的专用Scaffold + Widget _buildFaceToFaceScaffold(bool isDarkMode) { return Scaffold( backgroundColor: isDarkMode ? AppColors.darkBackground : Colors.grey[50], - appBar: _buildAppBar(isDarkMode), - body: _buildBody(isDarkMode), + body: Stack( + children: [ + // 主要内容区域 + FaceToFaceLayout( + isDarkMode: isDarkMode, + controller: controller, + ), + + // 顶部按钮(倒过来) + 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), + ), + + // 可拖拽的悬浮按钮 - 在所有模式下都显示 + DraggableFloatingButton( + isDarkMode: isDarkMode, + controller: controller, + ), + ], + ), + ); + } + + // 构建面对面翻译顶部按钮(原右边按钮,现在移到顶部并旋转) + 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, // 源语言名称 + ), ); } @@ -46,7 +141,7 @@ class TranslationView extends GetView { title: Obx(() => GestureDetector( onTap: () { // 点击标题时弹出翻译模式选择器 - _showTranslationModeSelector(isDarkMode); + TranslationModeSelector.show(isDarkMode); }, child: Text( controller.currentModeTitle.value.tr, @@ -149,7 +244,7 @@ class TranslationView extends GetView { // 源语言选择 Expanded( child: GestureDetector( - onTap: () => _showLanguageSelectionDialog(true, isDarkMode), + onTap: () => LanguageSelectionDialog.show(true, isDarkMode), child: Container( padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), @@ -197,7 +292,7 @@ class TranslationView extends GetView { // 目标语言选择 Expanded( child: GestureDetector( - onTap: () => _showLanguageSelectionDialog(false, isDarkMode), + onTap: () => LanguageSelectionDialog.show(false, isDarkMode), child: Container( padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), @@ -260,6 +355,7 @@ class TranslationView extends GetView { Widget _buildTranslationContent(bool isDarkMode) { return Obx(() { + // 其他模式使用原有的列表布局 final itemCount = controller.translationHistory.length + (controller.hasMoreHistory.value ? 1 : 0); @@ -474,7 +570,7 @@ class TranslationView extends GetView { controller.stopAll(); } else { // 弹出翻译模式选择器 - _showTranslationModeSelector(isDarkMode); + TranslationModeSelector.show(isDarkMode); } }, child: Container( @@ -654,158 +750,6 @@ class TranslationView extends GetView { ); } - // 显示语言选择对话框 - void _showLanguageSelectionDialog(bool isSource, bool isDarkMode) { - 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, - ), - ), - ), - ), - ], - ), - ), - ), - )); - } - // 构建加载更多指示器 Widget _buildLoadMoreIndicator() { return Obx(() { @@ -1046,158 +990,104 @@ class TranslationView extends GetView { } } - // 显示翻译模式选择器 - void _showTranslationModeSelector(bool isDarkMode) { - Get.bottomSheet( - BackdropFilter( - filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), - child: _buildTranslationModeSelector(), + // 构建语言选择栏 + 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), + ), + ], ), - isScrollControlled: true, - backgroundColor: Colors.transparent, - ); - } - - // 构建翻译模式选择器 - 参考会议模块样式 - Widget _buildTranslationModeSelector() { - return Builder( - builder: (BuildContext context) { - return SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _buildModeItem( - context, - 'simultaneousTranslation'.tr, - Icons.translate, - Colors.blue, - onTap: () async { - Get.back(); - await controller.stopAll(); - await controller.changeTranslationMode('simultaneous'); - await controller.startRecognition(); - }, - ), - _buildModeItem( - context, - 'faceToFaceTranslation'.tr, - Icons.people, - Colors.green, - onTap: () async { - Get.back(); - await controller.stopAll(); - await controller.changeTranslationMode('faceToFace'); - await controller.startRecognition(); - }, - ), - _buildModeItem( - context, - 'audioVideoTranslation'.tr, - Icons.videocam, - Colors.orange, - 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, - 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: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 源语言选择 + Expanded( + child: GestureDetector( + onTap: () => LanguageSelectionDialog.show(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, ), - child: const Icon(Icons.close, color: Colors.white), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, ), ), - if (Platform.isAndroid) SizedBox(height: 20.h), - ], + ), ), - ); - }, - ); - } - // 构建单个模式选项 - 完全参考会议模块的 _buildItem 方法 - Widget _buildModeItem( - BuildContext context, - String title, - IconData icon, - Color color, { - 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), + // 语言互换按钮 + GestureDetector( + onTap: controller.swapLanguages, + child: Container( + margin: EdgeInsets.symmetric(horizontal: 8.w), + padding: EdgeInsets.all(4.r), decoration: BoxDecoration( - color: color.withValues(alpha: onTap != null ? 0.3 : 0.2), - borderRadius: BorderRadius.circular(60.w), + color: isDarkMode + ? AppColors.primary.withOpacity(0.2) + : Colors.blue[50], + shape: BoxShape.circle, ), child: Icon( - icon, - color: color.withValues(alpha: onTap != null ? 1 : 0.5), + Icons.swap_horiz, + color: isDarkMode ? AppColors.primary : Colors.blue, + size: 20.sp, ), ), - Expanded( - child: Text( - title, - style: TextStyle( - fontSize: 15.sp, + ), + + // 目标语言选择 + Expanded( + child: GestureDetector( + onTap: () => LanguageSelectionDialog.show(false, isDarkMode), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), + decoration: BoxDecoration( color: isDarkMode - ? Colors.white.withValues(alpha: onTap != null ? 1 : 0.5) - : Colors.black87 - .withValues(alpha: onTap != null ? 1 : 0.5), - fontWeight: FontWeight.bold, + ? 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, + ), ), ), ), - Icon( - Icons.arrow_forward_ios, - size: 16.w, - color: Colors.grey.withValues(alpha: onTap != null ? 1 : 0.5), - ), - SizedBox(width: 5.w), - ], - ), + ), + ], ), ); } diff --git a/lib/modules/translation/views/widgets/draggable_floating_button.dart b/lib/modules/translation/views/widgets/draggable_floating_button.dart new file mode 100644 index 000000000..98b25adb3 --- /dev/null +++ b/lib/modules/translation/views/widgets/draggable_floating_button.dart @@ -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); + } +} diff --git a/lib/modules/translation/views/widgets/face_to_face_layout.dart b/lib/modules/translation/views/widgets/face_to_face_layout.dart new file mode 100644 index 000000000..562a543b9 --- /dev/null +++ b/lib/modules/translation/views/widgets/face_to_face_layout.dart @@ -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); + } +}