import 'dart:async'; import 'dart:io'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:image_picker/image_picker.dart'; import 'package:gal/gal.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; import 'package:permission_handler/permission_handler.dart'; import 'package:camera_app/services/yolo_service.dart'; import 'package:camera_app/screens/result_screen.dart'; class CameraScreen extends StatefulWidget { const CameraScreen({super.key}); @override State createState() => _CameraScreenState(); } class _CameraScreenState extends State { CameraController? _controller; List? _cameras; bool _isInitialized = false; bool _isCapturing = false; int _photoCount = 0; Timer? _timer; List _savedPhotoPaths = []; bool _isWebOrDesktop = false; final YOLOService _yoloService = YOLOService(); @override void initState() { super.initState(); _initializeCamera(); } Future _initializeCamera() async { try { // Web 和桌面平台使用 image_picker if (kIsWeb || (!Platform.isAndroid && !Platform.isIOS)) { if (mounted) { setState(() { _isWebOrDesktop = true; _isInitialized = true; // Web/桌面平台直接显示界面 }); } return; } // 移动平台使用 camera 包 await _requestPermissions(); _cameras = await availableCameras(); if (_cameras == null || _cameras!.isEmpty) { _showError('未找到可用的相机'); return; } _controller = CameraController( _cameras![0], ResolutionPreset.high, enableAudio: false, ); await _controller!.initialize(); if (mounted) { setState(() { _isInitialized = true; }); } } catch (e) { _showError('初始化相机失败: $e'); } } Future _requestPermissions() async { if (kIsWeb || (!Platform.isAndroid && !Platform.isIOS)) { return; // Web/桌面平台权限由浏览器/系统处理 } final cameraStatus = await Permission.camera.request(); if (!cameraStatus.isGranted) { _showError('需要相机权限才能使用此功能'); return; } if (Platform.isAndroid) { final photosStatus = await Permission.photos.request(); if (photosStatus.isDenied || photosStatus.isPermanentlyDenied) { final storageStatus = await Permission.storage.request(); if (!storageStatus.isGranted) { _showError('需要存储权限才能保存图片'); return; } } } else if (Platform.isIOS) { final photosStatus = await Permission.photos.request(); if (!photosStatus.isGranted) { _showError('需要照片权限才能保存图片'); return; } } } Future _startCapturing() async { if (_isWebOrDesktop) { _showError('Web/桌面平台不支持自动连续拍照,请使用移动设备'); return; } // 移动平台:自动拍照模式 if (!_isInitialized || _controller == null || !_controller!.value.isInitialized) { _showError('相机未初始化'); return; } setState(() { _isCapturing = true; _photoCount = 0; _savedPhotoPaths.clear(); }); // 立即拍摄第一张 await _takePhoto(); // 然后每2秒拍摄一张,直到点击停止 _timer = Timer.periodic(const Duration(seconds: 2), (timer) async { if (_isCapturing) { await _takePhoto(); } else { timer.cancel(); } }); } Future _stopCapturing() async { setState(() { _isCapturing = false; }); _timer?.cancel(); if (_savedPhotoPaths.isEmpty) { _showError('没有拍摄照片'); return; } // 显示分析中对话框 if (!mounted) return; showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return const AlertDialog( content: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(), SizedBox(height: 16), Text('正在分析照片...'), ], ), ); }, ); try { // 使用YOLO分析照片 final analysisResults = await _yoloService.analyzeImages(_savedPhotoPaths); if (!mounted) return; Navigator.of(context).pop(); // 关闭分析中对话框 // 导航到结果页面 Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (context) => ResultScreen( photoPaths: _savedPhotoPaths, analysisResults: analysisResults, ), ), ); } catch (e) { if (!mounted) return; Navigator.of(context).pop(); // 关闭分析中对话框 _showError('分析照片失败: $e'); } } Future _takePhoto() async { if (_controller == null || !_controller!.value.isInitialized) { return; } try { final XFile photo = await _controller!.takePicture(); await _savePhoto(photo.path); if (mounted) { setState(() { _photoCount++; }); } } catch (e) { if (mounted) { _showError('拍照失败: $e'); } } } Future _savePhoto(String photoPath) async { try { if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) { // 移动平台:保存到相册 await Gal.putImage(photoPath); } else { // Web/桌面平台:保存到下载目录 final appDir = await getApplicationDocumentsDirectory(); final fileName = 'photo_${DateTime.now().millisecondsSinceEpoch}.jpg'; final savedPath = path.join(appDir.path, fileName); await File(photoPath).copy(savedPath); } _savedPhotoPaths.add(photoPath); } catch (e) { // 保存失败不影响流程 _savedPhotoPaths.add(photoPath); } } void _showError(String message) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: Colors.red, ), ); } } @override void dispose() { _timer?.cancel(); _controller?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { if (!_isInitialized) { return Scaffold( backgroundColor: Colors.black, body: const Center( child: CircularProgressIndicator(color: Colors.white), ), ); } // Web/桌面平台界面 if (_isWebOrDesktop) { return Scaffold( backgroundColor: Colors.black, body: Center( child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.camera_alt, size: 100, color: Colors.white, ), const SizedBox(height: 32), Text( '$_photoCount 张', style: const TextStyle( color: Colors.white, fontSize: 48, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), const Text( 'Web/桌面平台不支持自动连续拍照', style: TextStyle( color: Colors.white70, fontSize: 18, ), textAlign: TextAlign.center, ), const SizedBox(height: 48), IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () => Navigator.of(context).pop(), tooltip: '返回', ), ], ), ), ), ); } // 移动平台:显示相机预览 if (_controller == null || !_controller!.value.isInitialized) { return Scaffold( backgroundColor: Colors.black, body: const Center( child: Text( '相机初始化中...', style: TextStyle(color: Colors.white), ), ), ); } return Scaffold( backgroundColor: Colors.black, body: Stack( children: [ SizedBox.expand(child: CameraPreview(_controller!)), SafeArea( child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black.withOpacity(0.7), Colors.transparent, ], ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () { if (_isCapturing) { _stopCapturing(); } Navigator.of(context).pop(); }, ), if (_isCapturing) Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), decoration: BoxDecoration( color: Colors.red.withOpacity(0.8), borderRadius: BorderRadius.circular(20), ), child: Row( children: [ Container( width: 12, height: 12, decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle, ), ), const SizedBox(width: 8), Text( '$_photoCount 张', style: const TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold, ), ), ], ), ), ], ), ), ), Align( alignment: Alignment.bottomCenter, child: Container( padding: const EdgeInsets.all(32), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, colors: [ Colors.black.withOpacity(0.7), Colors.transparent, ], ), ), child: _isCapturing ? Column( mainAxisSize: MainAxisSize.min, children: [ ElevatedButton.icon( onPressed: _stopCapturing, icon: const Icon(Icons.stop, size: 28), label: const Text( '停止拍照', style: TextStyle(fontSize: 18), ), style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: 32, vertical: 16, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30), ), ), ), const SizedBox(height: 16), Text( '已拍摄 $_photoCount 张照片', style: const TextStyle( color: Colors.white, fontSize: 16, ), ), ], ) : ElevatedButton.icon( onPressed: _startCapturing, icon: const Icon(Icons.camera_alt, size: 28), label: const Text( '开始拍照', style: TextStyle(fontSize: 18), ), style: ElevatedButton.styleFrom( backgroundColor: Colors.white, foregroundColor: Colors.black, padding: const EdgeInsets.symmetric( horizontal: 32, vertical: 16, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30), ), ), ), ), ), ], ), ); } }