import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:image_picker/image_picker.dart'; import '../controllers/picture_translation_controller.dart'; import '../models/picture_translation_models.dart'; import 'picture_translation_history_detail_view.dart'; class PictureTranslationView extends GetView { const PictureTranslationView({super.key}); @override Widget build(BuildContext context) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; return Scaffold( backgroundColor: Colors.grey[50], appBar: _buildAppBar(isDarkMode), 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(bool isDarkMode) { return AppBar( leading: IconButton( icon: Icon( Icons.arrow_back_ios, size: 20.w, color: isDarkMode ? Colors.white : Colors.black87, ), onPressed: () => Get.back(), ), centerTitle: true, title: Text( 'pictureTranslation'.tr, style: TextStyle( fontSize: 18.sp, fontWeight: FontWeight.w600, color: isDarkMode ? Colors.white : Colors.black, ), ), backgroundColor: isDarkMode ? Colors.black : Colors.white, foregroundColor: isDarkMode ? Colors.white : Colors.black87, elevation: 0, actions: [ IconButton( icon: const Icon(Icons.history), onPressed: _showHistoryBottomSheet, tooltip: '历史记录', ), PopupMenuButton( onSelected: _handleMenuAction, itemBuilder: (context) => [ const PopupMenuItem( value: 'clear', child: Row( children: [ Icon(Icons.clear_all, size: 20), SizedBox(width: 8), Text('清空结果'), ], ), ), const PopupMenuItem( value: 'settings', child: Row( children: [ Icon(Icons.settings, size: 20), SizedBox(width: 8), Text('设置'), ], ), ), ], ), ], ); } /// 构建快速操作区域 Widget _buildQuickActions() { return Container( padding: EdgeInsets.all(16.w), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12.r), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '选择图片来源', style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, color: Colors.black87, ), ), SizedBox(height: 12.h), Row( children: [ Expanded( child: _buildActionButton( icon: Icons.photo_library, label: '相册', onTap: controller.pickFromGallery, color: Colors.blue, ), ), SizedBox(width: 12.w), Expanded( child: _buildActionButton( icon: Icons.camera_alt, label: '拍照', onTap: controller.pickFromCamera, color: Colors.green, ), ), ], ), ], ), ); } /// 构建操作按钮 Widget _buildActionButton({ required IconData icon, required String label, required VoidCallback onTap, required Color color, }) { return Material( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(8.r), child: InkWell( onTap: controller.isProcessing.value ? null : onTap, borderRadius: BorderRadius.circular(8.r), child: Container( padding: EdgeInsets.symmetric(vertical: 16.h), child: Column( children: [ Icon( icon, color: color, size: 24.sp, ), SizedBox(height: 4.h), Text( label, style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.w500, color: color, ), ), ], ), ), ), ); } /// 构建语言选择器 Widget _buildLanguageSelector() { return Container( padding: EdgeInsets.all(16.w), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12.r), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '语言设置', style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, color: Colors.black87, ), ), SizedBox(height: 12.h), Row( children: [ Expanded( child: _buildLangTile( '源语言', controller.sourceLanguageChineseName.value, () => _showLangDialog(true), Colors.blue, ), ), SizedBox(width: 8.w), IconButton( onPressed: controller.swapLanguages, icon: const Icon(Icons.swap_horiz), tooltip: '交换语言', ), SizedBox(width: 8.w), Expanded( child: _buildLangTile( '目标语言', controller.targetLanguageChineseName.value, () => _showLangDialog(false), Colors.green, ), ), ], ), ], ), ); } Widget _buildLangTile( String label, String value, VoidCallback onTap, Color color) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(8.r), child: Container( padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 12.h), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8.r), border: Border.all(color: color.withOpacity(0.3)), color: color.withOpacity(0.05), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle(fontSize: 12.sp, color: Colors.grey[600]), ), SizedBox(height: 4.h), Text( value, style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.w600, color: color, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), ); } Future _showLangDialog(bool isSource) async { final langs = controller.languageManager.getAllChineseNames(); final selected = await Get.dialog( SimpleDialog( title: Text(isSource ? '选择源语言' : '选择目标语言'), children: [ SizedBox( width: 320.w, height: 420.h, child: ListView.builder( itemCount: langs.length, itemBuilder: (_, i) => ListTile( title: Text(langs[i]), onTap: () => Get.back(result: langs[i]), ), ), ) ], ), ); if (selected != null) { if (isSource) { controller.setSourceLanguage(selected); } else { controller.setTargetLanguage(selected); } } } Widget _buildImageSection() { return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12.r), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.all(16.w), child: Text( '图片预览', style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, color: Colors.black87, ), ), ), _buildImagePreview(), ], ), ); } Widget _buildProcessingIndicator() { final step = controller.currentStep.value; return Container( padding: EdgeInsets.all(16.w), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12.r), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ _buildStatusIcon(step.status), SizedBox(width: 12.w), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( step.title, style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, color: Colors.black87, ), ), if (step.description.isNotEmpty) ...[ SizedBox(height: 4.h), Text( step.description, style: TextStyle(fontSize: 14.sp, color: Colors.grey[600]), ), ], ], ), ), ], ), if (step.progress > 0 && step.status != ProcessingStatus.error) ...[ SizedBox(height: 12.h), LinearProgressIndicator( value: step.progress, backgroundColor: Colors.grey[200], valueColor: AlwaysStoppedAnimation(_getStatusColor(step.status)), ), ], ], ), ); } Widget _buildStatusIcon(ProcessingStatus status) { switch (status) { case ProcessingStatus.picking: case ProcessingStatus.recognizing: case ProcessingStatus.translating: case ProcessingStatus.saving: return SizedBox( width: 20.w, height: 20.w, child: CircularProgressIndicator( strokeWidth: 2, color: _getStatusColor(status), ), ); case ProcessingStatus.error: return Icon(Icons.error_outline, color: Colors.red, size: 20.sp); case ProcessingStatus.idle: return Icon(Icons.check_circle_outline, color: Colors.green, size: 20.sp); } } Color _getStatusColor(ProcessingStatus status) { switch (status) { case ProcessingStatus.picking: return Colors.blue; case ProcessingStatus.recognizing: return Colors.orange; case ProcessingStatus.translating: return Colors.purple; case ProcessingStatus.saving: return Colors.green; case ProcessingStatus.error: return Colors.red; case ProcessingStatus.idle: return Colors.grey; } } Widget _buildResultsSection() { return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12.r), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.all(16.w), child: Text( '翻译结果', style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, color: Colors.black87, ), ), ), if (controller.sourceText.value.isNotEmpty) _buildTextCard( '识别结果', controller.sourceText.value, controller.sourceLanguageChineseName.value, Colors.blue, ), if (controller.translatedText.value.isNotEmpty) _buildTextCard( '翻译结果', controller.translatedText.value, controller.targetLanguageChineseName.value, Colors.green, ), if (controller.translatedImageBytes.value != null) ...[ Padding( padding: EdgeInsets.symmetric(horizontal: 16.w), child: Text( '翻译后的图片预览', style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.w600, color: Colors.black87, ), ), ), SizedBox(height: 8.h), 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: ClipRRect( borderRadius: BorderRadius.circular(8.r), child: Image.memory( controller.translatedImageBytes.value!, 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 _buildTextCard( String title, String content, String language, Color color) { return Container( margin: EdgeInsets.symmetric(horizontal: 16.w).copyWith(bottom: 16.h), padding: EdgeInsets.all(16.w), decoration: BoxDecoration( color: color.withOpacity(0.05), borderRadius: BorderRadius.circular(8.r), border: Border.all(color: color.withOpacity(0.2)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '$title ($language)', style: TextStyle( fontSize: 14.sp, fontWeight: FontWeight.w600, color: color, ), ), IconButton( icon: Icon(Icons.copy, size: 18.sp, color: color), onPressed: () { Clipboard.setData(ClipboardData(text: content)); Get.snackbar('成功', '已复制到剪切板'); }, padding: EdgeInsets.zero, constraints: const BoxConstraints(), tooltip: '复制', ), ], ), SizedBox(height: 8.h), SelectableText( content, style: TextStyle(fontSize: 15.sp, color: Colors.black87, height: 1.4), ), ], ), ); } Widget _buildHistorySection() { return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12.r), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsets.all(16.w), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '最近翻译', style: TextStyle( fontSize: 16.sp, fontWeight: FontWeight.w600, color: Colors.black87, ), ), TextButton( onPressed: _showHistoryBottomSheet, child: const Text('查看全部'), ), ], ), ), if (controller.historyList.isEmpty) Padding( padding: EdgeInsets.symmetric(horizontal: 16.w).copyWith(bottom: 16.h), child: Text( '暂无翻译记录', style: TextStyle(fontSize: 14.sp, color: Colors.grey[600]), ), ) else ...controller.historyList .take(3) .map((item) => _buildHistoryItem(item)) .toList(), ], ), ); } Widget _buildHistoryItem(PictureTranslationHistory item) { return ListTile( leading: ClipRRect( borderRadius: BorderRadius.circular(4.r), child: Container( width: 40.w, height: 40.w, color: Colors.grey[200], child: Icon(Icons.image, color: Colors.grey[400]), ), ), title: Text( item.sourceText, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 14.sp), ), subtitle: Text( '${item.sourceLanguageName} → ${item.targetLanguageName}', style: TextStyle(fontSize: 12.sp, color: Colors.grey[600]), ), trailing: Text( _formatTime(item.timestamp), style: TextStyle(fontSize: 12.sp, color: Colors.grey[500]), ), onTap: () => _showHistoryDetail(item), ); } void _showHistoryBottomSheet() { Get.bottomSheet( Container( height: 600.h, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20.r)), ), child: Column( children: [ Container( padding: EdgeInsets.all(16.w), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '翻译历史', style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold), ), IconButton( onPressed: () => Get.back(), icon: const Icon(Icons.close), ), ], ), ), const Divider(height: 1), Expanded( child: Obx(() { if (controller.historyList.isEmpty) { return Center( child: Text( '暂无翻译记录', style: TextStyle(fontSize: 16.sp, color: Colors.grey[600]), ), ); } return ListView.separated( itemCount: controller.historyList.length, separatorBuilder: (context, index) => const Divider(height: 1), itemBuilder: (context, index) { final item = controller.historyList[index]; return _buildHistoryItem(item); }, ); }), ), ], ), ), isScrollControlled: true, ); } void _handleMenuAction(String action) { switch (action) { case 'clear': controller.reset(); break; case 'settings': _showSettingsDialog(); break; } } void _showHistoryDetail(PictureTranslationHistory item) { Get.to(() => PictureTranslationHistoryDetailView(item: item)); } String _formatTime(DateTime timestamp) { final now = DateTime.now(); final difference = now.difference(timestamp); if (difference.inDays > 0) { return '${difference.inDays}天前'; } else if (difference.inHours > 0) { return '${difference.inHours}小时前'; } else if (difference.inMinutes > 0) { return '${difference.inMinutes}分钟前'; } else { return '刚刚'; } } void _showSettingsDialog() { Get.dialog( AlertDialog( title: const Text('设置'), content: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(Icons.photo_size_select_actual), title: const Text('图片质量'), subtitle: Obx(() => Text('当前: ${controller.imageQuality.value}%')), onTap: () { Get.back(); _showImageQualityDialog(); }, ), ListTile( leading: const Icon(Icons.photo_size_select_large), title: const Text('最大尺寸'), subtitle: Obx(() => Text('当前: ${controller.maxImageSize.value}px')), onTap: () { Get.back(); _showImageSizeDialog(); }, ), ], ), actions: [ TextButton( onPressed: () => Get.back(), child: const Text('关闭'), ), ], ), ); } void _showImageQualityDialog() { Get.dialog( AlertDialog( title: const Text('设置图片质量'), content: Obx(() => Column( mainAxisSize: MainAxisSize.min, children: [ Text('当前质量: ${controller.imageQuality.value}%'), Slider( value: controller.imageQuality.value.toDouble(), min: 50, max: 100, divisions: 10, onChanged: (value) { controller.imageQuality.value = value.toInt(); }, ), ], )), actions: [ TextButton( onPressed: () => Get.back(), child: const Text('确定'), ), ], ), ); } void _showImageSizeDialog() { final sizes = [1280, 1920, 2560, 3840]; Get.dialog( AlertDialog( title: const Text('设置最大尺寸'), content: Column( mainAxisSize: MainAxisSize.min, children: sizes .map((size) => RadioListTile( title: Text('${size}px'), value: size, groupValue: controller.maxImageSize.value, onChanged: (value) { if (value != null) { controller.maxImageSize.value = value; Get.back(); } }, )) .toList(), ), actions: [ TextButton( onPressed: () => Get.back(), child: const Text('取消'), ), ], ), ); } void _showClearHistoryDialog() { Get.dialog( AlertDialog( title: const Text('清空历史'), content: const Text('确定要清空所有翻译历史吗?此操作不可撤销。'), actions: [ TextButton( onPressed: () => Get.back(), child: const Text('取消'), ), TextButton( onPressed: () { controller.clearHistory(); Get.back(); }, child: Text('确定', style: TextStyle(color: Colors.red)), ), ], ), ); } Widget _buildImagePreview() { final file = controller.pickedImageFile.value; return Container( height: 200.h, margin: EdgeInsets.symmetric(horizontal: 16.w).copyWith(bottom: 16.h), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(8.r), border: Border.all(color: Colors.grey[300]!), ), child: file == null ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.image_outlined, size: 48.sp, color: Colors.grey[400]), SizedBox(height: 8.h), Text( '请选择图片', style: TextStyle(fontSize: 14.sp, color: Colors.grey[600]), ), ], ), ) : ClipRRect( borderRadius: BorderRadius.circular(8.r), child: Image.file( file, fit: BoxFit.cover, width: double.infinity, errorBuilder: (context, error, stackTrace) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 48.sp, color: Colors.red[400]), SizedBox(height: 8.h), Text( '图片加载失败', style: TextStyle( fontSize: 14.sp, color: Colors.red[600]), ), ], ), ); }, ), ), ); } Widget _buildSourceTextBox() { return _buildTextBox('识别结果', controller.sourceText.value); } Widget _buildTranslatedTextBox() { return _buildTextBox('翻译结果', controller.translatedText.value); } Widget _buildTextBox(String title, String content) { return Container( padding: EdgeInsets.all(12.w), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8.r), border: Border.all(color: Colors.grey.shade300), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), SizedBox(height: 8.h), Text(content.isEmpty ? '—' : content), ], ), ); } }