Browse Source
添加图片翻译和文本翻译的完整功能实现,包括: 1. 新增翻译模块的路由和页面绑定 2. 实现图片翻译的控制器、模型和视图 3. 实现文本翻译的控制器和视图 4. 添加多语言支持 5. 完善翻译历史记录功能weicu
14 changed files with 2561 additions and 44 deletions
@ -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<PictureTranslationController>()) { |
|||
Get.lazyPut<PictureTranslationController>(() => PictureTranslationController()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
@ -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<VolcanoTranslationService>(); |
|||
final LanguageManager languageManager = Get.find<LanguageManager>(); |
|||
|
|||
// UI状态 |
|||
final sourceLanguageChineseName = '中文(简体)'.obs; |
|||
final targetLanguageChineseName = '英语'.obs; |
|||
final sourceText = ''.obs; |
|||
final translatedText = ''.obs; |
|||
final isProcessing = false.obs; |
|||
final pickedImageFile = Rx<File?>(null); |
|||
final currentStep = ProcessingStep( |
|||
title: '等待操作', |
|||
description: '请选择图片开始翻译', |
|||
status: ProcessingStatus.idle, |
|||
).obs; |
|||
|
|||
// 历史记录 |
|||
final historyList = <PictureTranslationHistory>[].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<String, dynamic>?; |
|||
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<dynamic>?; |
|||
if (historyData != null) { |
|||
historyList.value = historyData |
|||
.map((item) => PictureTranslationHistory.fromJson( |
|||
item as Map<String, dynamic>)) |
|||
.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<bool> _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<void> pickFromGallery() async { |
|||
//if (!(await _checkPermissions(ImageSource.gallery))) return; |
|||
await _pickAndProcess(ImageSource.gallery); |
|||
} |
|||
|
|||
/// 从相机拍照 |
|||
Future<void> pickFromCamera() async { |
|||
//if (!(await _checkPermissions(ImageSource.camera))) return; |
|||
await _pickAndProcess(ImageSource.camera); |
|||
} |
|||
|
|||
/// 选择并处理图片 |
|||
Future<void> _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<File> _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<void> _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<void> _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); |
|||
} |
|||
} |
|||
@ -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<String, dynamic> 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<String, dynamic> 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<LanguageManager>(); |
|||
return languageManager.getChineseNameByAsrCode(sourceLanguageCode) ?? |
|||
'未知语言'; |
|||
} |
|||
|
|||
/// 获取目标语言中文名称 |
|||
String get targetLanguageName { |
|||
final languageManager = Get.find<LanguageManager>(); |
|||
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, |
|||
); |
|||
} |
|||
} |
|||
@ -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('提示', '分享功能开发中'); |
|||
} |
|||
} |
|||
@ -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<PictureTranslationController> { |
|||
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<String>( |
|||
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<void> _showLangDialog(bool isSource) async { |
|||
final langs = controller.languageManager.getAllChineseNames(); |
|||
final selected = await Get.dialog<String>( |
|||
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<Color>(_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<int>( |
|||
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), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
} |
|||
@ -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>(() => TextTranslationController()); |
|||
} |
|||
} |
|||
@ -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<VolcanoTranslationService>(); |
|||
final LanguageManager _languageManager = Get.find<LanguageManager>(); |
|||
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<String> get supportedLanguages => _languageManager.getAllChineseNames(); |
|||
|
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
|
|||
// 初始化翻译服务 |
|||
_initializeTranslationService(); |
|||
|
|||
_loadSavedLanguages(); |
|||
|
|||
// 监听文本输入变化 |
|||
textController.addListener(() { |
|||
inputText.value = textController.text; |
|||
}); |
|||
} |
|||
|
|||
/// 初始化翻译服务 |
|||
Future<void> _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<void> 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, |
|||
); |
|||
} |
|||
} |
|||
@ -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<TextTranslationController> { |
|||
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<Color>(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, |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
}); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue