You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
401 lines
12 KiB
401 lines
12 KiB
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);
|
|
}
|
|
}
|
|
|