import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; // import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart'; import 'package:image_picker/image_picker.dart'; import 'package:image/image.dart' as img; import 'package:path/path.dart' as path; import 'package:permission_handler/permission_handler.dart'; import '../../../data/services/volcano_translation_service.dart'; import '../../../data/services/language_manager.dart'; import '../../../core/utils/logger.dart'; import '../models/picture_translation_models.dart'; class PictureTranslationController extends GetxController { final ImagePicker _imagePicker = ImagePicker(); // final TextRecognizer _textRecognizer = // TextRecognizer(script: TextRecognitionScript.chinese); final GetStorage _storage = GetStorage(); final VolcanoTranslationService _translationService = Get.find(); final LanguageManager languageManager = Get.find(); // UI状态 final sourceLanguageChineseName = 'autoDetect'.obs; final targetLanguageChineseName = '英语'.obs; final sourceText = ''.obs; final translatedText = ''.obs; final translatedImageBytes = Rx(null); // 回贴后的图片二进制 final isProcessing = false.obs; final pickedImageFile = Rx(null); final isSourceLanguageUserSet = false.obs; final isTargetLanguageUserSet = false.obs; final currentStep = ProcessingStep( title: 'waitingForAction'.tr, // 等待操作 description: 'pleaseSelectImageToStart'.tr, // 请选择图片开始翻译 status: ProcessingStatus.idle, ).obs; // 历史记录 final historyList = [].obs; final isLoadingHistory = false.obs; // 图片处理相关 final imageQuality = 85.obs; final maxImageSize = 1920.obs; static const String _historyKey = 'picture_translation_history'; static const String _settingsKey = 'picture_translation_settings'; 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?; 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?; if (historyData != null) { historyList.value = historyData .map((item) => PictureTranslationHistory.fromJson( item as Map)) .toList(); // 按时间倒序排列 historyList.sort((a, b) => b.timestamp.compareTo(a.timestamp)); } } catch (e) { Logger.error('PictureTranslation', '加载历史记录失败: $e'); } finally { isLoadingHistory.value = false; } } /// 保存历史记录 void _saveHistory() { try { final historyData = historyList.map((item) => item.toJson()).toList(); _storage.write(_historyKey, historyData); } catch (e) { Logger.error('PictureTranslation', '保存历史记录失败: $e'); } } /// 添加历史记录 void _addToHistory(PictureTranslationHistory history) { historyList.insert(0, history); // 限制历史记录数量,最多保存100条 if (historyList.length > 100) { historyList.removeRange(100, historyList.length); } _saveHistory(); } /// 删除历史记录 void deleteHistory(String id) { historyList.removeWhere((item) => item.id == id); _saveHistory(); // Get.snackbar('成功', '已删除历史记录'); } /// 清空历史记录 void clearHistory() { historyList.clear(); _storage.remove(_historyKey); // Get.snackbar('成功', '已清空所有历史记录'); } /// 检查权限 Future _checkPermissions(ImageSource source) async { if (source == ImageSource.camera) { final status = await Permission.camera.request(); if (!status.isGranted) { Get.snackbar( '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 pickFromGallery() async { //if (!(await _checkPermissions(ImageSource.gallery))) return; await _pickAndProcess(ImageSource.gallery); } /// 从相机拍照 Future pickFromCamera() async { //if (!(await _checkPermissions(ImageSource.camera))) return; await _pickAndProcess(ImageSource.camera); } /// 选择并处理图片 Future _pickAndProcess(ImageSource source) async { try { isProcessing.value = true; _updateStep( '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 _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 _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 _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); } }