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.

594 lines
19 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 = 'autoDetect'.obs;
final targetLanguageChineseName = '英语'.obs;
final sourceText = ''.obs;
final translatedText = ''.obs;
final translatedImageBytes = Rx<Uint8List?>(null); // 回贴后的图片二进制
final isProcessing = false.obs;
final pickedImageFile = Rx<File?>(null);
final isSourceLanguageUserSet = false.obs;
final isTargetLanguageUserSet = false.obs;
final currentStep = ProcessingStep(
title: 'waitingForAction'.tr, // 等待操作
description: 'pleaseSelectImageToStart'.tr, // 请选择图片开始翻译
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';
String? _lastDetectedSourceLanguageCode;
@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) {
final langs = languageManager.getAllChineseNames();
final appDefaultTargetLanguage = _getLanguageByAppLocale();
final bool sourceUserSet =
(settings['sourceLanguageUserSet'] == true) ||
((settings['sourceLanguageUserSet'] == null) &&
(settings['sourceLanguage'] != null) &&
(settings['sourceLanguage'] != '中文(简体)'));
bool targetUserSet = settings['targetLanguageUserSet'] == true;
final bool shouldMigrateTargetUserSetKey =
settings['targetLanguageUserSet'] == null;
if (shouldMigrateTargetUserSetKey) {
final savedTargetLanguage = settings['targetLanguage'];
if (savedTargetLanguage != null &&
appDefaultTargetLanguage != null &&
savedTargetLanguage == appDefaultTargetLanguage) {
settings.remove('targetLanguage');
targetUserSet = false;
} else {
targetUserSet = savedTargetLanguage != null &&
savedTargetLanguage != 'autoDetect' &&
savedTargetLanguage != '英语';
}
}
isSourceLanguageUserSet.value = sourceUserSet;
isTargetLanguageUserSet.value = targetUserSet;
final savedSourceLanguage = settings['sourceLanguage'];
if (sourceUserSet &&
savedSourceLanguage != null &&
(savedSourceLanguage == 'autoDetect' ||
langs.contains(savedSourceLanguage))) {
sourceLanguageChineseName.value = savedSourceLanguage;
} else {
sourceLanguageChineseName.value = 'autoDetect';
}
final savedTargetLanguage = settings['targetLanguage'];
if (targetUserSet &&
savedTargetLanguage != null &&
savedTargetLanguage != 'autoDetect' &&
langs.contains(savedTargetLanguage)) {
targetLanguageChineseName.value = savedTargetLanguage;
} else {
if (appDefaultTargetLanguage != null &&
langs.contains(appDefaultTargetLanguage)) {
targetLanguageChineseName.value = appDefaultTargetLanguage;
} else {
targetLanguageChineseName.value = '英语';
}
}
if (shouldMigrateTargetUserSetKey) {
_saveSettings();
}
imageQuality.value = settings['imageQuality'] ?? 85;
maxImageSize.value = settings['maxImageSize'] ?? 1920;
} else {
final appDefaultTargetLanguage = _getLanguageByAppLocale();
final langs = languageManager.getAllChineseNames();
if (appDefaultTargetLanguage != null &&
langs.contains(appDefaultTargetLanguage)) {
targetLanguageChineseName.value = appDefaultTargetLanguage;
} else {
targetLanguageChineseName.value = '英语';
}
}
} catch (e) {
Logger.error('PictureTranslation', '加载设置失败: $e');
}
}
/// 保存设置
void _saveSettings() {
try {
_storage.write(_settingsKey, {
if (isSourceLanguageUserSet.value)
'sourceLanguage': sourceLanguageChineseName.value,
if (isTargetLanguageUserSet.value)
'targetLanguage': targetLanguageChineseName.value,
'sourceLanguageUserSet': isSourceLanguageUserSet.value,
'targetLanguageUserSet': isTargetLanguageUserSet.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(
'permissionRequired'.tr, // 权限需求
'cameraPermissionRequired'.tr); // 需要相机权限才能拍照
return false;
}
} else {
final status = await Permission.photos.request();
if (!status.isGranted) {
Get.snackbar(
'permissionRequired'.tr, // 权限需求
'photoAlbumPermissionRequired'.tr); // 需要相册权限才能选择图片
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(
'chooseImage'.tr, // 选择图片
(source == ImageSource.camera
? 'openingCamera'.tr // 正在打开相机...
: 'openingPhotoAlbum'.tr), // 正在打开相册...
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(
'cancelled'.tr, // 已取消
'userCancelledSelection'.tr, // 用户取消了选择
ProcessingStatus.idle);
return;
}
File file = File(image.path);
// 图片预处理
file = await _preprocessImage(file);
pickedImageFile.value = file;
// 直接调用图片翻译API(包含识别+回贴)
_updateStep(
'translatingImage'.tr, // 翻译图片
'callingImageTranslateService'.tr, // 正在调用图片翻译服务...
ProcessingStatus.translating,
progress: 0.6);
await _performImageTranslation(file);
// 保存历史记录
if (sourceText.value.isNotEmpty && translatedText.value.isNotEmpty) {
_updateStep(
'savingRecord'.tr, // 保存记录
'savingTranslationResult'.tr, // 正在保存翻译结果...
ProcessingStatus.saving,
progress: 0.9);
await _saveTranslationResult(file);
}
_updateStep(
'translationCompleted'.tr, // 翻译完成
'allStepsCompleted'.tr, // 所有步骤已完成
ProcessingStatus.idle,
progress: 1.0);
} catch (e) {
Logger.error('PictureTranslation', '处理失败: $e');
_updateStep(
'processingFailed'.tr, // 处理失败
e.toString(),
ProcessingStatus.error);
_showErrorDialog(
'processingFailed'.tr, // 处理失败
'imageProcessingOrTranslationFailed'.tr.replaceAll(
'{error}', e.toString())); // 图片处理或翻译失败,请重试。错误信息: {error}
} 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; // 返回原图片
}
}
/// 执行图片翻译(调用火山引擎图片翻译API)
Future<void> _performImageTranslation(File imageFile) async {
try {
final bytes = await imageFile.readAsBytes();
// 图片大小限制:4MB(文档要求)
const int maxSize = 4 * 1024 * 1024;
if (bytes.lengthInBytes > maxSize) {
_updateStep(
'processingFailed'.tr, // 处理失败
'imageTooLarge'.tr, // 图片大小超过4MB限制
ProcessingStatus.error);
_showErrorDialog(
'processingFailed'.tr, // 处理失败
'imageTooLargeHint'.tr); // 图片大小超过4MB限制,请降低质量或尺寸后重试
return;
}
final targetCode = languageManager
.getAsrCodeByChineseName(targetLanguageChineseName.value) ??
'en-US';
final result = await _translationService.translateImage(
imageBytes: bytes,
targetLanguageCode: targetCode,
);
if (result == null) {
_updateStep(
'processingFailed'.tr, // 处理失败
'emptyTranslationResult'.tr, // 图片翻译接口返回空结果
ProcessingStatus.error);
_showErrorDialog(
'processingFailed'.tr, // 处理失败
'imageTranslationFailedTryLater'.tr); // 失败原因提示
return;
}
// 更新回贴图片
translatedImageBytes.value = result.translatedImageBytes != null
? Uint8List.fromList(result.translatedImageBytes!)
: null;
// 将文本块汇总为识别文本与翻译文本,便于展示与复制
if (result.blocks.isNotEmpty) {
sourceText.value = result.blocks.map((b) => b.text).join('\n');
translatedText.value =
result.blocks.map((b) => b.translation).join('\n');
} else {
sourceText.value = '';
translatedText.value = '';
}
_lastDetectedSourceLanguageCode =
_resolveDetectedSourceLanguageAsrCode(result);
} catch (e) {
Logger.error('PictureTranslation', '图片翻译调用失败: $e');
_updateStep(
'processingFailed'.tr, // 处理失败
e.toString(),
ProcessingStatus.error);
_showErrorDialog(
'processingFailed'.tr, // 处理失败
'imageTranslationApiException'.tr); // 接口调用异常
}
}
/// 保存翻译结果
Future<void> _saveTranslationResult(File imageFile) async {
try {
String sourceCode = 'zh-CN';
if (isSourceLanguageUserSet.value &&
sourceLanguageChineseName.value != 'autoDetect') {
sourceCode = languageManager
.getAsrCodeByChineseName(sourceLanguageChineseName.value) ??
'zh-CN';
} else if (_lastDetectedSourceLanguageCode != null &&
_lastDetectedSourceLanguageCode!.isNotEmpty) {
sourceCode = _lastDetectedSourceLanguageCode!;
}
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: Text('confirm'.tr), // 确认
),
],
),
);
}
/// 设置源语言
void setSourceLanguage(String language) {
if (sourceLanguageChineseName.value != language) {
sourceLanguageChineseName.value = language;
isSourceLanguageUserSet.value = language != 'autoDetect';
_saveSettings();
}
}
/// 设置目标语言
void setTargetLanguage(String language) {
if (targetLanguageChineseName.value != language) {
if (language == 'autoDetect') return;
targetLanguageChineseName.value = language;
isTargetLanguageUserSet.value = true;
_saveSettings();
}
}
/// 交换语言
void swapLanguages() {
if (sourceLanguageChineseName.value == 'autoDetect' ||
targetLanguageChineseName.value == 'autoDetect') {
return;
}
final temp = sourceLanguageChineseName.value;
sourceLanguageChineseName.value = targetLanguageChineseName.value;
targetLanguageChineseName.value = temp;
// 交换文本
final tempText = sourceText.value;
sourceText.value = translatedText.value;
translatedText.value = tempText;
isSourceLanguageUserSet.value = true;
isTargetLanguageUserSet.value = true;
_saveSettings();
}
/// 复制文本
void copyText(String text) {
if (text.isNotEmpty) {
// 这里可以添加复制到剪切板的逻辑
Get.snackbar(
'success'.tr, // 成功
'copied'.tr); // 已复制
}
}
/// 重置状态
void reset() {
sourceText.value = '';
translatedText.value = '';
translatedImageBytes.value = null;
pickedImageFile.value = null;
_updateStep(
'waitingForAction'.tr, // 等待操作
'pleaseSelectImageToStart'.tr, // 请选择图片开始翻译
ProcessingStatus.idle);
}
String? _getLanguageByAppLocale() {
final Locale? locale = Get.locale ?? Get.deviceLocale;
if (locale == null) return null;
final languageCode = locale.languageCode.toLowerCase();
final countryCode = locale.countryCode?.toUpperCase();
final scriptCode = locale.scriptCode;
if (languageCode == 'zh') {
if (countryCode == 'HK' || countryCode == 'MO') {
return languageManager.getChineseNameByAsrCode('zh-HK');
}
if (countryCode == 'TW') {
return languageManager.getChineseNameByAsrCode('zh-TW');
}
if (scriptCode == 'Hant') {
return languageManager.getChineseNameByAsrCode('zh-TW');
}
return languageManager.getChineseNameByAsrCode('zh-CN');
}
final normalizedShortCode = switch (languageCode) {
'nb' => 'no',
_ => languageCode,
};
return languageManager.getChineseNameByShortCode(normalizedShortCode);
}
String? _resolveDetectedSourceLanguageAsrCode(ImageTranslationResult result) {
final shortCode = result.detectedLanguage ??
(result.blocks.isNotEmpty
? result.blocks.first.detectedLanguage
: null);
if (shortCode == null || shortCode.isEmpty) return null;
return languageManager.getAsrCodeByShortCode(shortCode);
}
}