diff --git a/lib/core/translations/language/en_us.dart b/lib/core/translations/language/en_us.dart index e90b9f1d9..bd4ed2520 100644 --- a/lib/core/translations/language/en_us.dart +++ b/lib/core/translations/language/en_us.dart @@ -977,6 +977,8 @@ const Map enUS = { // 翻译模块 // 翻译中间层 (translation_middleware_view.dart) + "pictureTranslation": "Picture Translation", + "textTranslation": "Text Translation", // 翻译主页 "translationHistory": "Translation History", diff --git a/lib/core/translations/language/zh_cn.dart b/lib/core/translations/language/zh_cn.dart index cd06ee094..ee4db0321 100644 --- a/lib/core/translations/language/zh_cn.dart +++ b/lib/core/translations/language/zh_cn.dart @@ -897,6 +897,8 @@ const Map zhCN = { // 翻译模块 // 翻译中间层 (translation_middleware_view.dart) + "pictureTranslation": "图片翻译", + "textTranslation": "文本翻译", // 翻译主页 (translation_view.dart) "translationHistory": "翻译历史", diff --git a/lib/modules/picture_translation/bindings/picture_translation_binding.dart b/lib/modules/picture_translation/bindings/picture_translation_binding.dart new file mode 100644 index 000000000..459ad886a --- /dev/null +++ b/lib/modules/picture_translation/bindings/picture_translation_binding.dart @@ -0,0 +1,14 @@ +import 'package:get/get.dart'; + +import '../controllers/picture_translation_controller.dart'; + +class PictureTranslationBinding extends Bindings { + @override + void dependencies() { + if (!Get.isRegistered()) { + Get.lazyPut(() => PictureTranslationController()); + } + } +} + + diff --git a/lib/modules/picture_translation/controllers/picture_translation_controller.dart b/lib/modules/picture_translation/controllers/picture_translation_controller.dart new file mode 100644 index 000000000..5c8fed3fe --- /dev/null +++ b/lib/modules/picture_translation/controllers/picture_translation_controller.dart @@ -0,0 +1,401 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; +// import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:image/image.dart' as img; +import 'package:path/path.dart' as path; +import 'package:permission_handler/permission_handler.dart'; + +import '../../../data/services/volcano_translation_service.dart'; +import '../../../data/services/language_manager.dart'; +import '../../../core/utils/logger.dart'; +import '../models/picture_translation_models.dart'; + +class PictureTranslationController extends GetxController { + final ImagePicker _imagePicker = ImagePicker(); + // final TextRecognizer _textRecognizer = + // TextRecognizer(script: TextRecognitionScript.chinese); + final GetStorage _storage = GetStorage(); + + final VolcanoTranslationService _translationService = + Get.find(); + final LanguageManager languageManager = Get.find(); + + // UI状态 + final sourceLanguageChineseName = '中文(简体)'.obs; + final targetLanguageChineseName = '英语'.obs; + final sourceText = ''.obs; + final translatedText = ''.obs; + final isProcessing = false.obs; + final pickedImageFile = Rx(null); + final currentStep = ProcessingStep( + title: '等待操作', + description: '请选择图片开始翻译', + status: ProcessingStatus.idle, + ).obs; + + // 历史记录 + final historyList = [].obs; + final isLoadingHistory = false.obs; + + // 图片处理相关 + final imageQuality = 85.obs; + final maxImageSize = 1920.obs; + + static const String _historyKey = 'picture_translation_history'; + static const String _settingsKey = 'picture_translation_settings'; + + @override + void onInit() { + super.onInit(); + _loadSettings(); + _loadHistory(); + } + + @override + void onClose() { + // _textRecognizer.close(); + super.onClose(); + } + + /// 加载设置 + void _loadSettings() { + try { + final settings = _storage.read(_settingsKey) as Map?; + if (settings != null) { + sourceLanguageChineseName.value = + settings['sourceLanguage'] ?? '中文(简体)'; + targetLanguageChineseName.value = settings['targetLanguage'] ?? '英语'; + imageQuality.value = settings['imageQuality'] ?? 85; + maxImageSize.value = settings['maxImageSize'] ?? 1920; + } + } catch (e) { + Logger.error('PictureTranslation', '加载设置失败: $e'); + } + } + + /// 保存设置 + void _saveSettings() { + try { + _storage.write(_settingsKey, { + 'sourceLanguage': sourceLanguageChineseName.value, + 'targetLanguage': targetLanguageChineseName.value, + 'imageQuality': imageQuality.value, + 'maxImageSize': maxImageSize.value, + }); + } catch (e) { + Logger.error('PictureTranslation', '保存设置失败: $e'); + } + } + + /// 加载历史记录 + void _loadHistory() { + try { + isLoadingHistory.value = true; + final historyData = _storage.read(_historyKey) as List?; + if (historyData != null) { + historyList.value = historyData + .map((item) => PictureTranslationHistory.fromJson( + item as Map)) + .toList(); + // 按时间倒序排列 + historyList.sort((a, b) => b.timestamp.compareTo(a.timestamp)); + } + } catch (e) { + Logger.error('PictureTranslation', '加载历史记录失败: $e'); + } finally { + isLoadingHistory.value = false; + } + } + + /// 保存历史记录 + void _saveHistory() { + try { + final historyData = historyList.map((item) => item.toJson()).toList(); + _storage.write(_historyKey, historyData); + } catch (e) { + Logger.error('PictureTranslation', '保存历史记录失败: $e'); + } + } + + /// 添加历史记录 + void _addToHistory(PictureTranslationHistory history) { + historyList.insert(0, history); + // 限制历史记录数量,最多保存100条 + if (historyList.length > 100) { + historyList.removeRange(100, historyList.length); + } + _saveHistory(); + } + + /// 删除历史记录 + void deleteHistory(String id) { + historyList.removeWhere((item) => item.id == id); + _saveHistory(); + Get.snackbar('成功', '已删除历史记录'); + } + + /// 清空历史记录 + void clearHistory() { + historyList.clear(); + _storage.remove(_historyKey); + Get.snackbar('成功', '已清空所有历史记录'); + } + + /// 检查权限 + Future _checkPermissions(ImageSource source) async { + if (source == ImageSource.camera) { + final status = await Permission.camera.request(); + if (!status.isGranted) { + Get.snackbar('权限需求', '需要相机权限才能拍照'); + return false; + } + } else { + final status = await Permission.photos.request(); + if (!status.isGranted) { + Get.snackbar('权限需求', '需要相册权限才能选择图片'); + return false; + } + } + return true; + } + + /// 更新处理步骤 + void _updateStep(String title, String description, ProcessingStatus status, + {double progress = 0.0, String? error}) { + currentStep.value = ProcessingStep( + title: title, + description: description, + status: status, + progress: progress, + error: error, + ); + } + + /// 从相册选择 + Future pickFromGallery() async { + //if (!(await _checkPermissions(ImageSource.gallery))) return; + await _pickAndProcess(ImageSource.gallery); + } + + /// 从相机拍照 + Future pickFromCamera() async { + //if (!(await _checkPermissions(ImageSource.camera))) return; + await _pickAndProcess(ImageSource.camera); + } + + /// 选择并处理图片 + Future _pickAndProcess(ImageSource source) async { + try { + isProcessing.value = true; + _updateStep( + '选择图片', + '正在打开${source == ImageSource.camera ? '相机' : '相册'}...', + ProcessingStatus.picking); + + final XFile? image = await _imagePicker.pickImage( + source: source, + imageQuality: imageQuality.value, + maxWidth: maxImageSize.value.toDouble(), + maxHeight: maxImageSize.value.toDouble(), + ); + + if (image == null) { + _updateStep('已取消', '用户取消了选择', ProcessingStatus.idle); + return; + } + + File file = File(image.path); + + // 图片预处理 + file = await _preprocessImage(file); + pickedImageFile.value = file; + + _updateStep('识别文本', '正在分析图片中的文本...', ProcessingStatus.recognizing, + progress: 0.3); + + // 文本识别 + // final inputImage = InputImage.fromFile(file); + // final RecognizedText recognizedText = + // await _textRecognizer.processImage(inputImage); + + // sourceText.value = recognizedText.text.trim(); + + if (sourceText.value.isEmpty) { + _updateStep('识别完成', '未识别到任何文本', ProcessingStatus.idle); + translatedText.value = ''; + return; + } + + _updateStep('翻译文本', '正在翻译识别的文本...', ProcessingStatus.translating, + progress: 0.7); + + // 执行翻译 + await _performTranslation(); + + // 保存历史记录 + if (sourceText.value.isNotEmpty && translatedText.value.isNotEmpty) { + _updateStep('保存记录', '正在保存翻译结果...', ProcessingStatus.saving, + progress: 0.9); + await _saveTranslationResult(file); + } + + _updateStep('翻译完成', '所有步骤已完成', ProcessingStatus.idle, progress: 1.0); + } catch (e) { + Logger.error('PictureTranslation', '处理失败: $e'); + _updateStep('处理失败', e.toString(), ProcessingStatus.error); + _showErrorDialog('处理失败', '图片处理或翻译失败,请重试。\n错误信息: $e'); + } finally { + isProcessing.value = false; + } + } + + /// 图片预处理 + Future _preprocessImage(File file) async { + try { + final bytes = await file.readAsBytes(); + final image = img.decodeImage(bytes); + + if (image == null) return file; + + // 自动旋转图片 + final orientedImage = img.bakeOrientation(image); + + // 压缩图片大小(如果需要) + img.Image resizedImage = orientedImage; + if (orientedImage.width > maxImageSize.value || + orientedImage.height > maxImageSize.value) { + resizedImage = img.copyResize( + orientedImage, + width: orientedImage.width > orientedImage.height + ? maxImageSize.value + : null, + height: orientedImage.height > orientedImage.width + ? maxImageSize.value + : null, + ); + } + + // 保存处理后的图片 + final processedBytes = + img.encodeJpg(resizedImage, quality: imageQuality.value); + final processedFile = File('${file.path}_processed.jpg'); + await processedFile.writeAsBytes(processedBytes); + + return processedFile; + } catch (e) { + Logger.error('PictureTranslation', '图片预处理失败: $e'); + return file; // 返回原图片 + } + } + + /// 执行翻译 + Future _performTranslation() async { + final sourceCode = languageManager + .getAsrCodeByChineseName(sourceLanguageChineseName.value) ?? + 'zh-CN'; + final targetCode = languageManager + .getAsrCodeByChineseName(targetLanguageChineseName.value) ?? + 'en-US'; + + final result = await _translationService.translateText( + text: sourceText.value, + sourceLanguageCode: sourceCode, + targetLanguageCode: targetCode, + ); + + translatedText.value = result ?? ''; + } + + /// 保存翻译结果 + Future _saveTranslationResult(File imageFile) async { + try { + final sourceCode = languageManager + .getAsrCodeByChineseName(sourceLanguageChineseName.value) ?? + 'zh-CN'; + final targetCode = languageManager + .getAsrCodeByChineseName(targetLanguageChineseName.value) ?? + 'en-US'; + + final history = PictureTranslationHistory( + id: DateTime.now().millisecondsSinceEpoch.toString(), + imagePath: imageFile.path, + sourceText: sourceText.value, + translatedText: translatedText.value, + sourceLanguageCode: sourceCode, + targetLanguageCode: targetCode, + timestamp: DateTime.now(), + ); + + _addToHistory(history); + } catch (e) { + Logger.error('PictureTranslation', '保存翻译结果失败: $e'); + } + } + + /// 显示错误对话框 + void _showErrorDialog(String title, String message) { + Get.dialog( + AlertDialog( + title: Text(title), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('确定'), + ), + ], + ), + ); + } + + /// 设置源语言 + void setSourceLanguage(String language) { + if (sourceLanguageChineseName.value != language) { + sourceLanguageChineseName.value = language; + _saveSettings(); + } + } + + /// 设置目标语言 + void setTargetLanguage(String language) { + if (targetLanguageChineseName.value != language) { + targetLanguageChineseName.value = language; + _saveSettings(); + } + } + + /// 交换语言 + void swapLanguages() { + final temp = sourceLanguageChineseName.value; + sourceLanguageChineseName.value = targetLanguageChineseName.value; + targetLanguageChineseName.value = temp; + + // 交换文本 + final tempText = sourceText.value; + sourceText.value = translatedText.value; + translatedText.value = tempText; + + _saveSettings(); + } + + /// 复制文本 + void copyText(String text) { + if (text.isNotEmpty) { + // 这里可以添加复制到剪切板的逻辑 + Get.snackbar('成功', '文本已复制到剪切板'); + } + } + + /// 重置状态 + void reset() { + sourceText.value = ''; + translatedText.value = ''; + pickedImageFile.value = null; + _updateStep('等待操作', '请选择图片开始翻译', ProcessingStatus.idle); + } +} diff --git a/lib/modules/picture_translation/models/picture_translation_models.dart b/lib/modules/picture_translation/models/picture_translation_models.dart new file mode 100644 index 000000000..f0f24fadd --- /dev/null +++ b/lib/modules/picture_translation/models/picture_translation_models.dart @@ -0,0 +1,134 @@ +import 'package:get/get.dart'; +import '../../../data/services/language_manager.dart'; + +/// 图片翻译历史记录模型 +class PictureTranslationHistory { + final String id; + final String imagePath; + final String sourceText; + final String translatedText; + final String sourceLanguageCode; + final String targetLanguageCode; + final DateTime timestamp; + final String? thumbnailPath; // 缩略图路径 + + PictureTranslationHistory({ + required this.id, + required this.imagePath, + required this.sourceText, + required this.translatedText, + required this.sourceLanguageCode, + required this.targetLanguageCode, + required this.timestamp, + this.thumbnailPath, + }); + + /// 从JSON创建实例 + factory PictureTranslationHistory.fromJson(Map json) { + return PictureTranslationHistory( + id: json['id'] ?? '', + imagePath: json['imagePath'] ?? '', + sourceText: json['sourceText'] ?? '', + translatedText: json['translatedText'] ?? '', + sourceLanguageCode: json['sourceLanguageCode'] ?? 'zh-CN', + targetLanguageCode: json['targetLanguageCode'] ?? 'en-US', + timestamp: + DateTime.parse(json['timestamp'] ?? DateTime.now().toIso8601String()), + thumbnailPath: json['thumbnailPath'], + ); + } + + /// 转换为JSON + Map toJson() { + return { + 'id': id, + 'imagePath': imagePath, + 'sourceText': sourceText, + 'translatedText': translatedText, + 'sourceLanguageCode': sourceLanguageCode, + 'targetLanguageCode': targetLanguageCode, + 'timestamp': timestamp.toIso8601String(), + 'thumbnailPath': thumbnailPath, + }; + } + + /// 获取源语言中文名称 + String get sourceLanguageName { + final languageManager = Get.find(); + return languageManager.getChineseNameByAsrCode(sourceLanguageCode) ?? + '未知语言'; + } + + /// 获取目标语言中文名称 + String get targetLanguageName { + final languageManager = Get.find(); + return languageManager.getChineseNameByAsrCode(targetLanguageCode) ?? + '未知语言'; + } + + /// 创建副本 + PictureTranslationHistory copyWith({ + String? id, + String? imagePath, + String? sourceText, + String? translatedText, + String? sourceLanguageCode, + String? targetLanguageCode, + DateTime? timestamp, + String? thumbnailPath, + }) { + return PictureTranslationHistory( + id: id ?? this.id, + imagePath: imagePath ?? this.imagePath, + sourceText: sourceText ?? this.sourceText, + translatedText: translatedText ?? this.translatedText, + sourceLanguageCode: sourceLanguageCode ?? this.sourceLanguageCode, + targetLanguageCode: targetLanguageCode ?? this.targetLanguageCode, + timestamp: timestamp ?? this.timestamp, + thumbnailPath: thumbnailPath ?? this.thumbnailPath, + ); + } +} + +/// 处理状态枚举 +enum ProcessingStatus { + idle, // 空闲 + picking, // 选择图片 + recognizing, // 识别文本 + translating, // 翻译文本 + saving, // 保存记录 + error, // 错误状态 +} + +/// 处理步骤信息 +class ProcessingStep { + final String title; + final String description; + final ProcessingStatus status; + final double progress; + final String? error; + + ProcessingStep({ + required this.title, + required this.description, + required this.status, + this.progress = 0.0, + this.error, + }); + + ProcessingStep copyWith({ + String? title, + String? description, + ProcessingStatus? status, + double? progress, + String? error, + }) { + return ProcessingStep( + title: title ?? this.title, + description: description ?? this.description, + status: status ?? this.status, + progress: progress ?? this.progress, + error: error ?? this.error, + ); + } +} diff --git a/lib/modules/picture_translation/views/picture_translation_history_detail_view.dart b/lib/modules/picture_translation/views/picture_translation_history_detail_view.dart new file mode 100644 index 000000000..4a3e6147f --- /dev/null +++ b/lib/modules/picture_translation/views/picture_translation_history_detail_view.dart @@ -0,0 +1,299 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../models/picture_translation_models.dart'; + +class PictureTranslationHistoryDetailView extends StatelessWidget { + final PictureTranslationHistory item; + + const PictureTranslationHistoryDetailView({ + super.key, + required this.item, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + size: 20.w, + color: Colors.black87, + ), + onPressed: () => Get.back(), + ), + centerTitle: true, + title: const Text('翻译详情'), + backgroundColor: Colors.white, + foregroundColor: Colors.black87, + elevation: 0, + // actions: [ + // IconButton( + // icon: const Icon(Icons.share), + // onPressed: _shareTranslation, + // tooltip: '分享', + // ), + // ], + ), + body: SafeArea( + child: SingleChildScrollView( + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildImageSection(), + SizedBox(height: 16.h), + _buildLanguageInfo(), + SizedBox(height: 16.h), + _buildTextSection('识别结果', item.sourceText, Colors.blue), + SizedBox(height: 16.h), + _buildTextSection('翻译结果', item.translatedText, Colors.green), + SizedBox(height: 16.h), + _buildTimeInfo(), + ], + ), + ), + ), + ); + } + + Widget _buildImageSection() { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(16.w), + child: Text( + '原图片', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + ), + Container( + height: 250.h, + margin: + EdgeInsets.symmetric(horizontal: 16.w).copyWith(bottom: 16.h), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.r), + border: Border.all(color: Colors.grey[300]!), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8.r), + child: Image.file( + File(item.imagePath), + fit: BoxFit.contain, + width: double.infinity, + errorBuilder: (context, error, stackTrace) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, + size: 48.sp, color: Colors.red[400]), + SizedBox(height: 8.h), + Text( + '图片加载失败', + style: TextStyle( + fontSize: 14.sp, color: Colors.red[600]), + ), + ], + ), + ); + }, + ), + ), + ), + ], + ), + ); + } + + Widget _buildLanguageInfo() { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '源语言', + style: TextStyle(fontSize: 12.sp, color: Colors.grey[600]), + ), + SizedBox(height: 4.h), + Text( + item.sourceLanguageName, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.blue, + ), + ), + ], + ), + ), + Icon(Icons.arrow_forward, color: Colors.grey[400]), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '目标语言', + style: TextStyle(fontSize: 12.sp, color: Colors.grey[600]), + ), + SizedBox(height: 4.h), + Text( + item.targetLanguageName, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.green, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildTextSection(String title, String content, Color color) { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: color, + ), + ), + IconButton( + icon: Icon(Icons.copy, size: 20.sp, color: color), + onPressed: () { + Clipboard.setData(ClipboardData(text: content)); + Get.snackbar('成功', '已复制到剪切板'); + }, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + tooltip: '复制', + ), + ], + ), + SizedBox(height: 12.h), + SelectableText( + content, + style: TextStyle( + fontSize: 15.sp, + color: Colors.black87, + height: 1.5, + ), + ), + ], + ), + ); + } + + Widget _buildTimeInfo() { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '翻译时间', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + SizedBox(height: 8.h), + Text( + _formatDateTime(item.timestamp), + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey[600], + ), + ), + ], + ), + ); + } + + String _formatDateTime(DateTime dateTime) { + return '${dateTime.year}年${dateTime.month}月${dateTime.day}日 ' + '${dateTime.hour.toString().padLeft(2, '0')}:' + '${dateTime.minute.toString().padLeft(2, '0')}'; + } + + void _shareTranslation() { + final shareText = '原文:${item.sourceText}\n\n' + '译文:${item.translatedText}\n\n' + '翻译方向:${item.sourceLanguageName} → ${item.targetLanguageName}'; + + // 这里可以调用分享插件 + Get.snackbar('提示', '分享功能开发中'); + } +} diff --git a/lib/modules/picture_translation/views/picture_translation_view.dart b/lib/modules/picture_translation/views/picture_translation_view.dart new file mode 100644 index 000000000..26897ff3e --- /dev/null +++ b/lib/modules/picture_translation/views/picture_translation_view.dart @@ -0,0 +1,917 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:image_picker/image_picker.dart'; + +import '../controllers/picture_translation_controller.dart'; +import '../models/picture_translation_models.dart'; +import 'picture_translation_history_detail_view.dart'; + +class PictureTranslationView extends GetView { + const PictureTranslationView({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[50], + appBar: _buildAppBar(), + body: SafeArea( + child: Obx(() { + return CustomScrollView( + slivers: [ + SliverPadding( + padding: EdgeInsets.all(16.w), + sliver: SliverList( + delegate: SliverChildListDelegate([ + _buildQuickActions(), + SizedBox(height: 16.h), + _buildLanguageSelector(), + SizedBox(height: 16.h), + _buildImageSection(), + SizedBox(height: 16.h), + if (controller.currentStep.value.status != + ProcessingStatus.idle) + _buildProcessingIndicator(), + if (controller.sourceText.value.isNotEmpty || + controller.translatedText.value.isNotEmpty) ...[ + SizedBox(height: 16.h), + _buildResultsSection(), + ], + SizedBox(height: 16.h), + _buildHistorySection(), + ]), + ), + ), + ], + ); + }), + ), + ); + } + + /// 构建应用栏 + PreferredSizeWidget _buildAppBar() { + return AppBar( + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios, + size: 20.w, + color: Colors.black87, + ), + onPressed: () => Get.back(), + ), + centerTitle: true, + title: const Text('图片翻译'), + backgroundColor: Colors.white, + foregroundColor: Colors.black87, + elevation: 0, + actions: [ + IconButton( + icon: const Icon(Icons.history), + onPressed: _showHistoryBottomSheet, + tooltip: '历史记录', + ), + PopupMenuButton( + onSelected: _handleMenuAction, + itemBuilder: (context) => [ + const PopupMenuItem( + value: 'clear', + child: Row( + children: [ + Icon(Icons.clear_all, size: 20), + SizedBox(width: 8), + Text('清空结果'), + ], + ), + ), + const PopupMenuItem( + value: 'settings', + child: Row( + children: [ + Icon(Icons.settings, size: 20), + SizedBox(width: 8), + Text('设置'), + ], + ), + ), + ], + ), + ], + ); + } + + /// 构建快速操作区域 + Widget _buildQuickActions() { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '选择图片来源', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + SizedBox(height: 12.h), + Row( + children: [ + Expanded( + child: _buildActionButton( + icon: Icons.photo_library, + label: '相册', + onTap: controller.pickFromGallery, + color: Colors.blue, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: _buildActionButton( + icon: Icons.camera_alt, + label: '拍照', + onTap: controller.pickFromCamera, + color: Colors.green, + ), + ), + ], + ), + ], + ), + ); + } + + /// 构建操作按钮 + Widget _buildActionButton({ + required IconData icon, + required String label, + required VoidCallback onTap, + required Color color, + }) { + return Material( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(8.r), + child: InkWell( + onTap: controller.isProcessing.value ? null : onTap, + borderRadius: BorderRadius.circular(8.r), + child: Container( + padding: EdgeInsets.symmetric(vertical: 16.h), + child: Column( + children: [ + Icon( + icon, + color: color, + size: 24.sp, + ), + SizedBox(height: 4.h), + Text( + label, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + color: color, + ), + ), + ], + ), + ), + ), + ); + } + + /// 构建语言选择器 + Widget _buildLanguageSelector() { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '语言设置', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + SizedBox(height: 12.h), + Row( + children: [ + Expanded( + child: _buildLangTile( + '源语言', + controller.sourceLanguageChineseName.value, + () => _showLangDialog(true), + Colors.blue, + ), + ), + SizedBox(width: 8.w), + IconButton( + onPressed: controller.swapLanguages, + icon: const Icon(Icons.swap_horiz), + tooltip: '交换语言', + ), + SizedBox(width: 8.w), + Expanded( + child: _buildLangTile( + '目标语言', + controller.targetLanguageChineseName.value, + () => _showLangDialog(false), + Colors.green, + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildLangTile( + String label, String value, VoidCallback onTap, Color color) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(8.r), + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.r), + border: Border.all(color: color.withOpacity(0.3)), + color: color.withOpacity(0.05), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle(fontSize: 12.sp, color: Colors.grey[600]), + ), + SizedBox(height: 4.h), + Text( + value, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: color, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ); + } + + Future _showLangDialog(bool isSource) async { + final langs = controller.languageManager.getAllChineseNames(); + final selected = await Get.dialog( + SimpleDialog( + title: Text(isSource ? '选择源语言' : '选择目标语言'), + children: [ + SizedBox( + width: 320.w, + height: 420.h, + child: ListView.builder( + itemCount: langs.length, + itemBuilder: (_, i) => ListTile( + title: Text(langs[i]), + onTap: () => Get.back(result: langs[i]), + ), + ), + ) + ], + ), + ); + if (selected != null) { + if (isSource) { + controller.setSourceLanguage(selected); + } else { + controller.setTargetLanguage(selected); + } + } + } + + Widget _buildImageSection() { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(16.w), + child: Text( + '图片预览', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + ), + _buildImagePreview(), + ], + ), + ); + } + + Widget _buildProcessingIndicator() { + final step = controller.currentStep.value; + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + _buildStatusIcon(step.status), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + step.title, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + if (step.description.isNotEmpty) ...[ + SizedBox(height: 4.h), + Text( + step.description, + style: + TextStyle(fontSize: 14.sp, color: Colors.grey[600]), + ), + ], + ], + ), + ), + ], + ), + if (step.progress > 0 && step.status != ProcessingStatus.error) ...[ + SizedBox(height: 12.h), + LinearProgressIndicator( + value: step.progress, + backgroundColor: Colors.grey[200], + valueColor: + AlwaysStoppedAnimation(_getStatusColor(step.status)), + ), + ], + ], + ), + ); + } + + Widget _buildStatusIcon(ProcessingStatus status) { + switch (status) { + case ProcessingStatus.picking: + case ProcessingStatus.recognizing: + case ProcessingStatus.translating: + case ProcessingStatus.saving: + return SizedBox( + width: 20.w, + height: 20.w, + child: CircularProgressIndicator( + strokeWidth: 2, + color: _getStatusColor(status), + ), + ); + case ProcessingStatus.error: + return Icon(Icons.error_outline, color: Colors.red, size: 20.sp); + case ProcessingStatus.idle: + return Icon(Icons.check_circle_outline, + color: Colors.green, size: 20.sp); + } + } + + Color _getStatusColor(ProcessingStatus status) { + switch (status) { + case ProcessingStatus.picking: + return Colors.blue; + case ProcessingStatus.recognizing: + return Colors.orange; + case ProcessingStatus.translating: + return Colors.purple; + case ProcessingStatus.saving: + return Colors.green; + case ProcessingStatus.error: + return Colors.red; + case ProcessingStatus.idle: + return Colors.grey; + } + } + + Widget _buildResultsSection() { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(16.w), + child: Text( + '翻译结果', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + ), + if (controller.sourceText.value.isNotEmpty) + _buildTextCard( + '识别结果', + controller.sourceText.value, + controller.sourceLanguageChineseName.value, + Colors.blue, + ), + if (controller.translatedText.value.isNotEmpty) + _buildTextCard( + '翻译结果', + controller.translatedText.value, + controller.targetLanguageChineseName.value, + Colors.green, + ), + ], + ), + ); + } + + Widget _buildTextCard( + String title, String content, String language, Color color) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 16.w).copyWith(bottom: 16.h), + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: color.withOpacity(0.05), + borderRadius: BorderRadius.circular(8.r), + border: Border.all(color: color.withOpacity(0.2)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '$title ($language)', + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: color, + ), + ), + IconButton( + icon: Icon(Icons.copy, size: 18.sp, color: color), + onPressed: () { + Clipboard.setData(ClipboardData(text: content)); + Get.snackbar('成功', '已复制到剪切板'); + }, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + tooltip: '复制', + ), + ], + ), + SizedBox(height: 8.h), + SelectableText( + content, + style: + TextStyle(fontSize: 15.sp, color: Colors.black87, height: 1.4), + ), + ], + ), + ); + } + + Widget _buildHistorySection() { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(16.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '最近翻译', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + TextButton( + onPressed: _showHistoryBottomSheet, + child: const Text('查看全部'), + ), + ], + ), + ), + if (controller.historyList.isEmpty) + Padding( + padding: + EdgeInsets.symmetric(horizontal: 16.w).copyWith(bottom: 16.h), + child: Text( + '暂无翻译记录', + style: TextStyle(fontSize: 14.sp, color: Colors.grey[600]), + ), + ) + else + ...controller.historyList + .take(3) + .map((item) => _buildHistoryItem(item)) + .toList(), + ], + ), + ); + } + + Widget _buildHistoryItem(PictureTranslationHistory item) { + return ListTile( + leading: ClipRRect( + borderRadius: BorderRadius.circular(4.r), + child: Container( + width: 40.w, + height: 40.w, + color: Colors.grey[200], + child: Icon(Icons.image, color: Colors.grey[400]), + ), + ), + title: Text( + item.sourceText, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 14.sp), + ), + subtitle: Text( + '${item.sourceLanguageName} → ${item.targetLanguageName}', + style: TextStyle(fontSize: 12.sp, color: Colors.grey[600]), + ), + trailing: Text( + _formatTime(item.timestamp), + style: TextStyle(fontSize: 12.sp, color: Colors.grey[500]), + ), + onTap: () => _showHistoryDetail(item), + ); + } + + void _showHistoryBottomSheet() { + Get.bottomSheet( + Container( + height: 600.h, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)), + ), + child: Column( + children: [ + Container( + padding: EdgeInsets.all(16.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '翻译历史', + style: + TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold), + ), + IconButton( + onPressed: () => Get.back(), + icon: const Icon(Icons.close), + ), + ], + ), + ), + const Divider(height: 1), + Expanded( + child: Obx(() { + if (controller.historyList.isEmpty) { + return Center( + child: Text( + '暂无翻译记录', + style: + TextStyle(fontSize: 16.sp, color: Colors.grey[600]), + ), + ); + } + return ListView.separated( + itemCount: controller.historyList.length, + separatorBuilder: (context, index) => + const Divider(height: 1), + itemBuilder: (context, index) { + final item = controller.historyList[index]; + return _buildHistoryItem(item); + }, + ); + }), + ), + ], + ), + ), + isScrollControlled: true, + ); + } + + void _handleMenuAction(String action) { + switch (action) { + case 'clear': + controller.reset(); + break; + case 'settings': + _showSettingsDialog(); + break; + } + } + + void _showHistoryDetail(PictureTranslationHistory item) { + Get.to(() => PictureTranslationHistoryDetailView(item: item)); + } + + String _formatTime(DateTime timestamp) { + final now = DateTime.now(); + final difference = now.difference(timestamp); + + if (difference.inDays > 0) { + return '${difference.inDays}天前'; + } else if (difference.inHours > 0) { + return '${difference.inHours}小时前'; + } else if (difference.inMinutes > 0) { + return '${difference.inMinutes}分钟前'; + } else { + return '刚刚'; + } + } + + void _showSettingsDialog() { + Get.dialog( + AlertDialog( + title: const Text('设置'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.photo_size_select_actual), + title: const Text('图片质量'), + subtitle: + Obx(() => Text('当前: ${controller.imageQuality.value}%')), + onTap: () { + Get.back(); + _showImageQualityDialog(); + }, + ), + ListTile( + leading: const Icon(Icons.photo_size_select_large), + title: const Text('最大尺寸'), + subtitle: + Obx(() => Text('当前: ${controller.maxImageSize.value}px')), + onTap: () { + Get.back(); + _showImageSizeDialog(); + }, + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('关闭'), + ), + ], + ), + ); + } + + void _showImageQualityDialog() { + Get.dialog( + AlertDialog( + title: const Text('设置图片质量'), + content: Obx(() => Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('当前质量: ${controller.imageQuality.value}%'), + Slider( + value: controller.imageQuality.value.toDouble(), + min: 50, + max: 100, + divisions: 10, + onChanged: (value) { + controller.imageQuality.value = value.toInt(); + }, + ), + ], + )), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('确定'), + ), + ], + ), + ); + } + + void _showImageSizeDialog() { + final sizes = [1280, 1920, 2560, 3840]; + Get.dialog( + AlertDialog( + title: const Text('设置最大尺寸'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: sizes + .map((size) => RadioListTile( + title: Text('${size}px'), + value: size, + groupValue: controller.maxImageSize.value, + onChanged: (value) { + if (value != null) { + controller.maxImageSize.value = value; + Get.back(); + } + }, + )) + .toList(), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('取消'), + ), + ], + ), + ); + } + + void _showClearHistoryDialog() { + Get.dialog( + AlertDialog( + title: const Text('清空历史'), + content: const Text('确定要清空所有翻译历史吗?此操作不可撤销。'), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('取消'), + ), + TextButton( + onPressed: () { + controller.clearHistory(); + Get.back(); + }, + child: Text('确定', style: TextStyle(color: Colors.red)), + ), + ], + ), + ); + } + + Widget _buildImagePreview() { + final file = controller.pickedImageFile.value; + return Container( + height: 200.h, + margin: EdgeInsets.symmetric(horizontal: 16.w).copyWith(bottom: 16.h), + decoration: BoxDecoration( + color: Colors.grey[100], + borderRadius: BorderRadius.circular(8.r), + border: Border.all(color: Colors.grey[300]!), + ), + child: file == null + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.image_outlined, + size: 48.sp, color: Colors.grey[400]), + SizedBox(height: 8.h), + Text( + '请选择图片', + style: TextStyle(fontSize: 14.sp, color: Colors.grey[600]), + ), + ], + ), + ) + : ClipRRect( + borderRadius: BorderRadius.circular(8.r), + child: Image.file( + file, + fit: BoxFit.cover, + width: double.infinity, + errorBuilder: (context, error, stackTrace) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline, + size: 48.sp, color: Colors.red[400]), + SizedBox(height: 8.h), + Text( + '图片加载失败', + style: TextStyle( + fontSize: 14.sp, color: Colors.red[600]), + ), + ], + ), + ); + }, + ), + ), + ); + } + + Widget _buildSourceTextBox() { + return _buildTextBox('识别结果', controller.sourceText.value); + } + + Widget _buildTranslatedTextBox() { + return _buildTextBox('翻译结果', controller.translatedText.value); + } + + Widget _buildTextBox(String title, String content) { + return Container( + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.r), + border: Border.all(color: Colors.grey.shade300), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), + SizedBox(height: 8.h), + Text(content.isEmpty ? '—' : content), + ], + ), + ); + } +} diff --git a/lib/modules/text_translation/bindings/text_translation_binding.dart b/lib/modules/text_translation/bindings/text_translation_binding.dart new file mode 100644 index 000000000..6060b5318 --- /dev/null +++ b/lib/modules/text_translation/bindings/text_translation_binding.dart @@ -0,0 +1,11 @@ +import 'package:get/get.dart'; +import '../controllers/text_translation_controller.dart'; + +/// 文本翻译页面依赖绑定 +class TextTranslationBinding extends Bindings { + @override + void dependencies() { + // 注册文本翻译控制器 + Get.lazyPut(() => TextTranslationController()); + } +} diff --git a/lib/modules/text_translation/controllers/text_translation_controller.dart b/lib/modules/text_translation/controllers/text_translation_controller.dart new file mode 100644 index 000000000..e8ed2e33d --- /dev/null +++ b/lib/modules/text_translation/controllers/text_translation_controller.dart @@ -0,0 +1,336 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; + +import '../../../data/services/volcano_translation_service.dart'; +import '../../../data/services/language_manager.dart'; +import '../../../core/utils/logger.dart'; + +/// 文本翻译控制器 +class TextTranslationController extends GetxController { + // 依赖服务 + final VolcanoTranslationService _translationService = Get.find(); + final LanguageManager _languageManager = Get.find(); + final GetStorage _storage = GetStorage(); + + // 文本输入控制器 + final TextEditingController textController = TextEditingController(); + + // 响应式状态 + final sourceLanguageChineseName = '中文(简体)'.obs; + final targetLanguageChineseName = '英语'.obs; + final sourceLanguageCode = 'zh-CN'.obs; + final targetLanguageCode = 'en-US'.obs; + final translatedText = ''.obs; + final isTranslating = false.obs; + final inputText = ''.obs; + + // 支持的语言列表 + List get supportedLanguages => _languageManager.getAllChineseNames(); + + @override + void onInit() { + super.onInit(); + + // 初始化翻译服务 + _initializeTranslationService(); + + _loadSavedLanguages(); + + // 监听文本输入变化 + textController.addListener(() { + inputText.value = textController.text; + }); + } + + /// 初始化翻译服务 + Future _initializeTranslationService() async { + try { + final initialized = await _translationService.initialize(); + if (!initialized) { + Logger.error('翻译服务初始化失败'); + Get.snackbar( + 'error'.tr, + '翻译服务初始化失败,请检查网络连接', + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.9), + colorText: Colors.white, + ); + } else { + Logger.info('翻译服务初始化成功'); + } + } catch (e) { + Logger.error('翻译服务初始化异常: $e'); + } + } + + @override + void onClose() { + textController.dispose(); + super.onClose(); + } + + /// 加载保存的语言设置 + void _loadSavedLanguages() { + try { + final savedSourceLanguage = _storage.read('text_translation_source_language'); + final savedTargetLanguage = _storage.read('text_translation_target_language'); + + if (savedSourceLanguage != null && supportedLanguages.contains(savedSourceLanguage)) { + sourceLanguageChineseName.value = savedSourceLanguage; + // 同时更新语言代码 + final asrCode = _languageManager.getAsrCodeByChineseName(savedSourceLanguage); + if (asrCode != null) { + sourceLanguageCode.value = asrCode; + } + } + + if (savedTargetLanguage != null && supportedLanguages.contains(savedTargetLanguage)) { + targetLanguageChineseName.value = savedTargetLanguage; + // 同时更新语言代码 + final asrCode = _languageManager.getAsrCodeByChineseName(savedTargetLanguage); + if (asrCode != null) { + targetLanguageCode.value = asrCode; + } + } + } catch (e) { + Logger.error('加载语言设置失败: $e'); + } + } + + /// 保存语言设置 + void _saveLanguageSettings() { + try { + _storage.write('text_translation_source_language', sourceLanguageChineseName.value); + _storage.write('text_translation_target_language', targetLanguageChineseName.value); + } catch (e) { + Logger.error('保存语言设置失败: $e'); + } + } + + /// 设置源语言 + void setSourceLanguage(String language) { + if (language != sourceLanguageChineseName.value) { + sourceLanguageChineseName.value = language; + + // 同时更新语言代码 + final asrCode = _languageManager.getAsrCodeByChineseName(language); + if (asrCode != null) { + sourceLanguageCode.value = asrCode; + } + + _saveLanguageSettings(); + + // 如果已有翻译结果,清空结果 + if (translatedText.value.isNotEmpty) { + translatedText.value = ''; + } + } + } + + /// 设置目标语言 + void setTargetLanguage(String language) { + if (language != targetLanguageChineseName.value) { + targetLanguageChineseName.value = language; + + // 同时更新语言代码 + final asrCode = _languageManager.getAsrCodeByChineseName(language); + if (asrCode != null) { + targetLanguageCode.value = asrCode; + } + + _saveLanguageSettings(); + + // 如果已有翻译结果,清空结果 + if (translatedText.value.isNotEmpty) { + translatedText.value = ''; + } + } + } + + /// 交换源语言和目标语言 + void swapLanguages() { + final tempSource = sourceLanguageChineseName.value; + final tempTarget = targetLanguageChineseName.value; + final tempSourceCode = sourceLanguageCode.value; + final tempTargetCode = targetLanguageCode.value; + final tempInputText = textController.text; + final tempTranslatedText = translatedText.value; + + sourceLanguageChineseName.value = tempTarget; + targetLanguageChineseName.value = tempSource; + sourceLanguageCode.value = tempTargetCode; + targetLanguageCode.value = tempSourceCode; + + // 交换输入文本和翻译结果 + textController.text = tempTranslatedText; + translatedText.value = tempInputText; + + _saveLanguageSettings(); + } + + /// 执行翻译 + Future translateText() async { + final text = textController.text.trim(); + + // 验证输入 + if (text.isEmpty) { + Get.snackbar( + 'error'.tr, + 'pleaseInputText'.tr, + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.orange.withOpacity(0.9), + colorText: Colors.white, + ); + return; + } + + if (text.length > 1000) { + Get.snackbar( + 'error'.tr, + 'textTooLong'.tr, + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.orange.withOpacity(0.9), + colorText: Colors.white, + ); + return; + } + + // 检查源语言和目标语言是否相同 + if (sourceLanguageChineseName.value == targetLanguageChineseName.value) { + translatedText.value = text; + return; + } + + isTranslating.value = true; + translatedText.value = ''; + + try { + Logger.info('开始翻译: $text'); + Logger.info('源语言: ${sourceLanguageChineseName.value} -> ${sourceLanguageCode.value}'); + Logger.info('目标语言: ${targetLanguageChineseName.value} -> ${targetLanguageCode.value}'); + + // 确保翻译服务已初始化 + Logger.info('确保翻译服务已初始化...'); + final initialized = await _translationService.initialize(); + Logger.info('翻译服务初始化结果: $initialized'); + if (!initialized) { + throw Exception('翻译服务初始化失败'); + } + + Logger.info('调用翻译服务...'); + + // 调用翻译服务,直接使用维护的语言代码 + final result = await _translationService.translateText( + text: text, + sourceLanguageCode: sourceLanguageCode.value, + targetLanguageCode: targetLanguageCode.value, + ); + + Logger.info('翻译服务返回结果: $result'); + + if (result != null && result.isNotEmpty) { + translatedText.value = result; + Logger.info('翻译成功: $result'); + } else { + Logger.error('翻译服务返回空结果'); + throw Exception('翻译服务返回空结果'); + } + } catch (e) { + Logger.error('翻译失败: $e'); + Get.snackbar( + 'error'.tr, + 'translationFailed'.tr, + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.red.withOpacity(0.9), + colorText: Colors.white, + ); + } finally { + isTranslating.value = false; + } + } + + /// 复制翻译结果 + void copyResult() { + if (translatedText.value.isNotEmpty) { + Clipboard.setData(ClipboardData(text: translatedText.value)); + Get.snackbar( + 'success'.tr, + 'copied'.tr, + snackPosition: SnackPosition.TOP, + backgroundColor: Colors.green.withOpacity(0.9), + colorText: Colors.white, + duration: const Duration(seconds: 2), + ); + } + } + + /// 清空输入 + void clearInput() { + textController.clear(); + translatedText.value = ''; + } + + /// 显示语言选择对话框 + void showLanguageSelector({required bool isSourceLanguage}) { + Get.bottomSheet( + Container( + height: Get.height * 0.6, + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + isSourceLanguage ? 'sourceLanguage'.tr : 'targetLanguage'.tr, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + IconButton( + onPressed: () => Get.back(), + icon: const Icon(Icons.close), + ), + ], + ), + ), + const Divider(height: 1), + Expanded( + child: ListView.builder( + itemCount: supportedLanguages.length, + itemBuilder: (context, index) { + final language = supportedLanguages[index]; + final isSelected = isSourceLanguage + ? language == sourceLanguageChineseName.value + : language == targetLanguageChineseName.value; + + return ListTile( + title: Text(language), + trailing: isSelected ? const Icon(Icons.check, color: Colors.blue) : null, + onTap: () { + if (isSourceLanguage) { + setSourceLanguage(language); + } else { + setTargetLanguage(language); + } + Get.back(); + }, + ); + }, + ), + ), + ], + ), + ), + isScrollControlled: true, + ); + } +} diff --git a/lib/modules/text_translation/views/text_translation_view.dart b/lib/modules/text_translation/views/text_translation_view.dart new file mode 100644 index 000000000..294046c79 --- /dev/null +++ b/lib/modules/text_translation/views/text_translation_view.dart @@ -0,0 +1,395 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +import '../controllers/text_translation_controller.dart'; + +/// 文本翻译页面 +class TextTranslationView extends GetView { + const TextTranslationView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + + return Scaffold( + backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], + appBar: _buildAppBar(isDarkMode), + body: SafeArea( + child: SingleChildScrollView( + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 语言选择区域 + _buildLanguageSelector(isDarkMode), + SizedBox(height: 20.h), + + // 输入区域 + _buildInputSection(isDarkMode), + SizedBox(height: 20.h), + + // 翻译按钮 + _buildTranslateButton(isDarkMode), + SizedBox(height: 20.h), + + // 翻译结果区域 + _buildResultSection(isDarkMode), + ], + ), + ), + ), + ); + } + + /// 构建应用栏 + PreferredSizeWidget _buildAppBar(bool isDarkMode) { + return AppBar( + backgroundColor: isDarkMode ? Colors.black : Colors.white, + elevation: 0, + leading: IconButton( + icon: Icon( + Icons.arrow_back, + color: isDarkMode ? Colors.white : Colors.black, + ), + onPressed: () => Get.back(), + ), + title: Text( + 'textTranslation'.tr, + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black, + fontSize: 18.sp, + fontWeight: FontWeight.w600, + ), + ), + actions: [ + IconButton( + icon: Icon( + Icons.clear_all, + color: isDarkMode ? Colors.white : Colors.black, + ), + onPressed: controller.clearInput, + tooltip: 'clearInput'.tr, + ), + ], + ); + } + + /// 构建语言选择器 + Widget _buildLanguageSelector(bool isDarkMode) { + return Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + children: [ + // 源语言选择 + Obx(() => _buildLanguageButton( + label: 'sourceLanguage'.tr, + selectedLanguage: controller.sourceLanguageChineseName.value, + onTap: () => controller.showLanguageSelector(isSourceLanguage: true), + isDarkMode: isDarkMode, + )), + + SizedBox(height: 12.h), + + // 交换语言按钮 + Center( + child: GestureDetector( + onTap: controller.swapLanguages, + child: Container( + width: 40.w, + height: 40.w, + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon( + Icons.swap_vert, + color: Colors.blue, + size: 20.sp, + ), + ), + ), + ), + + SizedBox(height: 12.h), + + // 目标语言选择 + Obx(() => _buildLanguageButton( + label: 'targetLanguage'.tr, + selectedLanguage: controller.targetLanguageChineseName.value, + onTap: () => controller.showLanguageSelector(isSourceLanguage: false), + isDarkMode: isDarkMode, + )), + ], + ), + ); + } + + /// 构建语言选择按钮 + Widget _buildLanguageButton({ + required String label, + required String selectedLanguage, + required VoidCallback onTap, + required bool isDarkMode, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[800] : Colors.grey[100], + borderRadius: BorderRadius.circular(8.r), + border: Border.all( + color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 12.sp, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ), + SizedBox(height: 4.h), + Text( + selectedLanguage, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + ], + ), + Icon( + Icons.keyboard_arrow_down, + color: isDarkMode ? Colors.grey[400] : Colors.grey[600], + ), + ], + ), + ), + ); + } + + /// 构建输入区域 + Widget _buildInputSection(bool isDarkMode) { + return Container( + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.fromLTRB(16.w, 16.h, 16.w, 8.h), + child: Text( + 'inputTextToTranslate'.tr, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.grey[300] : Colors.grey[700], + ), + ), + ), + Container( + margin: EdgeInsets.symmetric(horizontal: 16.w), + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[800] : Colors.grey[50], + borderRadius: BorderRadius.circular(8.r), + border: Border.all( + color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!, + ), + ), + child: TextField( + controller: controller.textController, + maxLines: 6, + maxLength: 1000, + style: TextStyle( + fontSize: 16.sp, + color: isDarkMode ? Colors.white : Colors.black, + ), + decoration: InputDecoration( + hintText: 'inputTextToTranslate'.tr, + hintStyle: TextStyle( + color: isDarkMode ? Colors.grey[500] : Colors.grey[400], + ), + border: InputBorder.none, + counterStyle: TextStyle( + color: isDarkMode ? Colors.grey[500] : Colors.grey[400], + ), + ), + ), + ), + SizedBox(height: 16.h), + ], + ), + ); + } + + /// 构建翻译按钮 + Widget _buildTranslateButton(bool isDarkMode) { + return Obx(() => SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: controller.isTranslating.value ? null : controller.translateText, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + padding: EdgeInsets.symmetric(vertical: 16.h), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.r), + ), + elevation: 0, + disabledBackgroundColor: Colors.grey, + ), + child: controller.isTranslating.value + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 20.w, + height: 20.w, + child: const CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ), + SizedBox(width: 12.w), + Text( + 'translating'.tr, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + ], + ) + : Text( + 'translate'.tr, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + )); + } + + /// 构建翻译结果区域 + Widget _buildResultSection(bool isDarkMode) { + return Obx(() { + if (controller.translatedText.value.isEmpty) { + return const SizedBox.shrink(); + } + + return Container( + width: double.infinity, + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[900] : Colors.white, + borderRadius: BorderRadius.circular(12.r), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'translationResult'.tr, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + color: isDarkMode ? Colors.grey[300] : Colors.grey[700], + ), + ), + GestureDetector( + onTap: controller.copyResult, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.1), + borderRadius: BorderRadius.circular(6.r), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.copy, + size: 16.sp, + color: Colors.blue, + ), + SizedBox(width: 4.w), + Text( + 'copyResult'.tr, + style: TextStyle( + fontSize: 12.sp, + color: Colors.blue, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ], + ), + SizedBox(height: 12.h), + Container( + width: double.infinity, + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: isDarkMode ? Colors.grey[800] : Colors.grey[50], + borderRadius: BorderRadius.circular(8.r), + border: Border.all( + color: isDarkMode ? Colors.grey[700]! : Colors.grey[300]!, + ), + ), + child: Text( + controller.translatedText.value, + style: TextStyle( + fontSize: 16.sp, + color: isDarkMode ? Colors.white : Colors.black, + height: 1.5, + ), + ), + ), + ], + ), + ); + }); + } +} diff --git a/lib/modules/translation/middleware/controllers/translation_middleware_controller.dart b/lib/modules/translation/middleware/controllers/translation_middleware_controller.dart index ef260f152..e77e12172 100644 --- a/lib/modules/translation/middleware/controllers/translation_middleware_controller.dart +++ b/lib/modules/translation/middleware/controllers/translation_middleware_controller.dart @@ -9,36 +9,50 @@ class TranslationMiddlewareController extends GetxController { final List translationOptions = [ TranslationOption( translationId: 'simultaneous', - title: 'simultaneousTranslation', // 实时翻译 - description: 'realtimeTranslationDescription', // 实时语音翻译,支持多种语言 + title: 'simultaneousTranslation'.tr, // 实时翻译 + // description: 'realtimeTranslationDescription', // 实时语音翻译,支持多种语言 icon: Icons.record_voice_over, iconColor: 0xFF2196F3, // 蓝色 targetRoute: Routes.translation, ), TranslationOption( translationId: 'call', - title: 'callTranslation', // 电话翻译 - description: 'simultaneousTranslationInfo', // 双向对话翻译,面对面交流 + title: 'callTranslation'.tr, // 通话翻译 + // description: 'simultaneousTranslationInfo', // 双向对话翻译,面对面交流 icon: Icons.call, iconColor: 0xFF4CAF50, // 绿色 targetRoute: Routes.translation, ), TranslationOption( translationId: 'faceToFace', - title: 'faceToFaceTranslation', // 面对面翻译 - description: 'faceToFaceTranslationDescription', // 面对面交流,支持多种语言 + title: 'faceToFaceTranslation'.tr, // 面对面翻译 + // description: 'faceToFaceTranslationDescription', // 面对面交流,支持多种语言 icon: Icons.people, iconColor: 0xFFFF9800, // 橙色 targetRoute: Routes.translation, ), TranslationOption( translationId: 'audioVideo', - title: 'audioVideoTranslation', // 多媒体翻译 - description: 'audioVideoTranslationDescription', // 支持音频和视频翻译 + title: 'audioVideoTranslation'.tr, // 多媒体翻译 + //description: 'audioVideoTranslationDescription', // 支持音频和视频翻译 icon: Icons.videocam, iconColor: 0xFF9C27B0, // 紫色 targetRoute: Routes.translation, ), + TranslationOption( + translationId: 'picture_translator', + title: 'pictureTranslation'.tr, // 图片翻译 + icon: Icons.image, + iconColor: 0xFF009688, // 青色 + targetRoute: Routes.pictureTranslation, + ), + TranslationOption( + translationId: 'text_translator', + title: 'textTranslation'.tr, // 文本翻译 + icon: Icons.text_fields, + iconColor: 0xFF3F51B5, // 棕色 + targetRoute: Routes.textTranslation, + ), ]; /// 导航到指定的翻译功能 @@ -54,7 +68,8 @@ class TranslationMiddlewareController extends GetxController { /// 获取翻译选项的本地化描述 String getLocalizedDescription(TranslationOption option) { - return option.description.tr; + // return option.description.tr; + return ''; } } @@ -62,7 +77,7 @@ class TranslationMiddlewareController extends GetxController { class TranslationOption { final String translationId; final String title; - final String description; + // final String description; final IconData icon; final int iconColor; final String targetRoute; @@ -70,7 +85,7 @@ class TranslationOption { TranslationOption({ required this.translationId, required this.title, - required this.description, + // required this.description, required this.icon, required this.iconColor, required this.targetRoute, diff --git a/lib/modules/translation/middleware/views/translation_middleware_view.dart b/lib/modules/translation/middleware/views/translation_middleware_view.dart index fad64e403..5e727dcc0 100644 --- a/lib/modules/translation/middleware/views/translation_middleware_view.dart +++ b/lib/modules/translation/middleware/views/translation_middleware_view.dart @@ -38,8 +38,8 @@ class TranslationMiddlewareView title: Text( 'interpretation'.tr, // 翻译 style: TextStyle( - fontSize: 20.sp, - fontWeight: FontWeight.bold, + fontSize: 18.sp, + fontWeight: FontWeight.w600, color: isDarkMode ? Colors.white : Colors.black, ), ), @@ -125,7 +125,7 @@ class TranslationMiddlewareView // 标题 Text( - controller.getLocalizedTitle(option), + option.title, style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.bold, @@ -136,39 +136,10 @@ class TranslationMiddlewareView overflow: TextOverflow.ellipsis, ), //SizedBox(height: 8.h), - - // 描述 - // Text( - // controller.getLocalizedDescription(option), - // style: TextStyle( - // fontSize: 12.sp, - // color: isDarkMode ? Colors.grey[400] : Colors.grey[600], - // height: 1.3, - // ), - // textAlign: TextAlign.center, - // maxLines: 3, - // overflow: TextOverflow.ellipsis, - // ), ], ), ), ), ); } - - /// 根据图标名称获取图标数据 - IconData _getIconData(String iconName) { - switch (iconName) { - case 'translate': - return Icons.translate; - case 'people': - return Icons.people; - case 'history': - return Icons.history; - case 'bar_chart': - return Icons.bar_chart; - default: - return Icons.help_outline; - } - } } diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index 234fc6d38..f16b5c0a3 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -67,6 +67,10 @@ import '../modules/payment/bindings/payment_binding.dart'; import '../modules/payment/views/payment_view.dart'; import '../modules/translation/middleware/views/translation_middleware_view.dart'; import '../modules/translation/middleware/bindings/translation_middleware_binding.dart'; +import '../modules/text_translation/bindings/text_translation_binding.dart'; +import '../modules/text_translation/views/text_translation_view.dart'; +import '../modules/picture_translation/views/picture_translation_view.dart'; +import '../modules/picture_translation/bindings/picture_translation_binding.dart'; abstract class AppPages { static final pages = [ @@ -164,6 +168,20 @@ abstract class AppPages { transition: Transition.rightToLeft, transitionDuration: Duration(milliseconds: 250), ), + GetPage( + name: Routes.pictureTranslation, + page: () => const PictureTranslationView(), + binding: PictureTranslationBinding(), + transition: Transition.rightToLeft, + transitionDuration: Duration(milliseconds: 250), + ), + GetPage( + name: Routes.textTranslation, + page: () => const TextTranslationView(), + binding: TextTranslationBinding(), + transition: Transition.rightToLeft, + transitionDuration: Duration(milliseconds: 250), + ), GetPage( name: Routes.translationHistory, page: () => const TranslationHistoryView(), diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index da88b0ed7..b513e0369 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -27,7 +27,9 @@ abstract class Routes { static const meetingRecord = '/meeting/record'; // 语言功能 static const translationMiddleware = '/translationMiddleware'; // 翻译中间层(统一入口) - static const translation = '/translation'; // 实时翻译 + static const translation = '/translation'; // 翻译 + static const pictureTranslation = '/picture_translation'; // 图片翻译 + static const textTranslation = '/text_translation'; // 文本翻译 static const translationHistory = '/translation/history'; // 翻译历史 static const translationStats = '/translation/stats'; // 翻译统计 static const speechDemo = '/speech_demo'; // 语音演示