import 'dart:io'; import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:camera/camera.dart'; // AJOUT : Pour piloter le flux caméra en direct import 'package:provider/provider.dart'; import '../../core/constants/app_constants.dart'; import '../../core/theme/app_theme.dart'; import '../../data/models/target_type.dart'; import '../crop/crop_screen.dart'; import '../session/session_provider.dart'; import 'widgets/image_source_button.dart'; import '../../services/image_crop_service.dart'; class CaptureScreen extends StatefulWidget { const CaptureScreen({super.key}); @override State createState() => _CaptureScreenState(); } class _CaptureScreenState extends State with SingleTickerProviderStateMixin { final ImagePicker _picker = ImagePicker(); final ImageCropService _cropService = ImageCropService(); final TargetType _selectedType = TargetType.concentric; String? _selectedImagePath; bool _isLoading = false; // AJOUT : Variables de gestion du flux caméra en direct CameraController? _cameraController; List? _cameras; bool _isCameraInitialized = false; bool _showLiveCamera = false; // AJOUT : Animation pour l'effet "pulsation de détection" du cadre vert late AnimationController _scanAnimationController; late Animation _scanAnimation; @override void initState() { super.initState(); // Initialisation de l'animation du viseur vert (effet pulsation) _scanAnimationController = AnimationController( duration: const Duration(seconds: 2), vsync: this, )..repeat(reverse: true); _scanAnimation = Tween(begin: 0.8, end: 1.0).animate( CurvedAnimation(parent: _scanAnimationController, curve: Curves.easeInOut), ); } @override void dispose() { _cameraController?.dispose(); _scanAnimationController.dispose(); super.dispose(); } // AJOUT : Initialisation de la caméra embarquée Future _initLiveCamera() async { final status = await Permission.camera.request(); if (!status.isGranted) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Accès à l\'appareil photo requis')), ); } return; } setState(() => _isLoading = true); try { _cameras = await availableCameras(); if (_cameras != null && _cameras!.isNotEmpty) { _cameraController = CameraController( _cameras![0], ResolutionPreset.max, // Force le 4:3 natif au maximum du capteur enableAudio: false, imageFormatGroup: ImageFormatGroup.jpeg, ); // INITIALISATION UNIQUE ET PROPRE : await _cameraController!.initialize(); setState(() { _isCameraInitialized = true; _showLiveCamera = true; }); } } catch (e) { debugPrint('Erreur caméra: $e'); } finally { setState(() => _isLoading = false); } } Future _handleGallerySelection() async { PermissionStatus status; if (Platform.isAndroid) { final deviceInfo = DeviceInfoPlugin(); final androidInfo = await deviceInfo.androidInfo; if (androidInfo.version.sdkInt >= 33) { status = await Permission.photos.request(); } else { status = await Permission.storage.request(); } } else { status = await Permission.photos.request(); } if (status.isGranted) { _captureImageFromGallery(); } else if (status.isPermanentlyDenied) { _showSettingsDialog(); } else { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Accès à la galerie requis pour continuer'), ), ); } } } void _showSettingsDialog() { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Permission requise'), content: const Text( 'L\'accès aux photos est nécessaire. Veuillez l\'activer dans les paramètres.', ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Annuler'), ), TextButton( onPressed: () => openAppSettings(), child: const Text('Paramètres'), ), ], ), ); } @override Widget build(BuildContext context) { final sessionProvider = context.watch(); final title = sessionProvider.isSessionActive ? 'Cible ${sessionProvider.targetCount + 1}' : 'Source'; // AJOUT : Si l'utilisateur clique sur scanner, on affiche la caméra plein écran avec l'overlay vert if (_showLiveCamera && _cameraController != null && _cameraController!.value.isInitialized) { return _buildLiveCameraView(); } return Scaffold( appBar: AppBar(title: Text(title)), body: SingleChildScrollView( padding: const EdgeInsets.all(AppConstants.defaultPadding), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const SizedBox(height: AppConstants.largePadding), _buildSectionTitle('Source de l\'Image'), const SizedBox(height: 12), Row( children: [ Expanded( child: ImageSourceButton( icon: Icons.camera_alt, label: 'Scanner', onPressed: _isLoading ? null : _initLiveCamera, // Ouvre notre caméra intégrée ), ), const SizedBox(width: 12), Expanded( child: ImageSourceButton( icon: Icons.photo_library, label: 'Galerie', onPressed: _isLoading ? null : _handleGallerySelection, ), ), ], ), const SizedBox(height: AppConstants.largePadding), if (_isLoading) const Center( child: Padding( padding: EdgeInsets.all(32), child: CircularProgressIndicator(), ), ) else _buildGuide(), ], ), ), ); } // AJOUT : Interface de la caméra live avec l'assistant de cadrage vert // INTERFACE CORRIGÉE : Cadre fixe et cible ronde verte au centre // INTERFACE : Restauration de TON ancienne cible centrale verte d'origine Widget _buildLiveCameraView() { return Scaffold( backgroundColor: Colors.black, body: Stack( fit: StackFit.expand, children: [ // 1. Le flux vidéo de la caméra Center( child: AspectRatio( aspectRatio: 3 / 4, // Aligné sur le format natif du capteur photo child: CameraPreview(_cameraController!), ), ), // 2. L'ASSISTANT DE CADRAGE VIRTUEL (Avec le grand cadre complet et la petite cible) Center( child: Container( width: MediaQuery.of(context).size.width * 0.85, height: MediaQuery.of(context).size.width * 0.85, decoration: BoxDecoration( border: Border.all( color: const Color(0xFF00FF00), // Grand cadre extérieur vert fluo width: 2.0, ), borderRadius: BorderRadius.circular(16), ), child: Stack( children: [ // SÉCURITÉ : On réintègre les 4 coins renforcés sur les côtés ! _buildCameraCorner(TopLeft: true), _buildCameraCorner(TopRight: true), _buildCameraCorner(BottomLeft: true), _buildCameraCorner(BottomRight: true), // TA MIRE CENTRALE HISTORIQUE FINALE Center( child: Container( width: 24, // Diamètre global de la petite cible height: 24, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: const Color(0xFF00FF00), width: 1.2, // Ligne fine ), ), child: Stack( children: [ // Trait horizontal fin confiné Center( child: Container( width: 20, height: 1.2, color: const Color(0xFF00FF00), ), ), // Trait vertical fin confiné Center( child: Container( width: 1.2, height: 20, color: const Color(0xFF00FF00), ), ), ], ), ), ), ], ), ), ), // 3. Barre supérieure avec bouton Retour Positioned( top: 40, left: 20, child: CircleAvatar( backgroundColor: Colors.black54, child: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () { setState(() { _showLiveCamera = false; }); }, ), ), ), // 4. Consigne de scan textuelle Positioned( top: 50, left: 0, right: 0, child: Center( child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.7), borderRadius: BorderRadius.circular(20), ), child: const Text( 'ALIGNEZ LA CIBLE DANS LE CADRE', style: TextStyle( color: Color(0xFF00FF00), fontWeight: FontWeight.bold, fontSize: 12, letterSpacing: 1.2, ), ), ), ), ), // 5. Bouton déclencheur Manuel en bas Positioned( bottom: 40, left: 0, right: 0, child: Center( child: GestureDetector( onTap: _takePictureManually, child: Container( height: 80, width: 80, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 4), ), child: Center( child: Container( height: 64, width: 64, decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle, ), ), ), ), ), ), ), ], ), ); } Widget _buildCameraCorner({ bool TopLeft = false, bool TopRight = false, bool BottomLeft = false, bool BottomRight = false, }) { return Positioned( top: (TopLeft || TopRight) ? 10 : null, bottom: (BottomLeft || BottomRight) ? 10 : null, left: (TopLeft || BottomLeft) ? 10 : null, right: (TopRight || BottomRight) ? 10 : null, child: Container( width: 20, height: 20, decoration: BoxDecoration( border: Border( top: (TopLeft || TopRight) ? const BorderSide(color: Color(0xFF00FF00), width: 4) : BorderSide.none, bottom: (BottomLeft || BottomRight) ? const BorderSide(color: Color(0xFF00FF00), width: 4) : BorderSide.none, left: (TopLeft || BottomLeft) ? const BorderSide(color: Color(0xFF00FF00), width: 4) : BorderSide.none, right: (TopRight || BottomRight) ? const BorderSide(color: Color(0xFF00FF00), width: 4) : BorderSide.none, ), ), ), ); } // AJOUT : Capture manuelle via notre bouton déclencheur Future _takePictureManually() async { if (_cameraController == null || !_cameraController!.value.isInitialized) return; setState(() => _isLoading = true); try { // 1. On prend la photo brute normale (sans y toucher) final XFile photo = await _cameraController!.takePicture(); setState(() { _selectedImagePath = photo.path; _showLiveCamera = false; }); // 2. On l'envoie directement à l'analyse _analyzeImage(); } catch (e) { debugPrint('Erreur lors du clic photo: $e'); } finally { setState(() => _isLoading = false); } } Widget _buildSectionTitle(String title) { return Text( title, style: Theme.of( context, ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ); } Widget _buildGuide() { return Card( child: Padding( padding: const EdgeInsets.all(AppConstants.defaultPadding), child: Column( children: [ Icon(Icons.help_outline, size: 48, color: Colors.grey[400]), const SizedBox(height: 12), Text( 'Conseils pour une bonne analyse', style: Theme.of( context, ).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 12), _buildGuideItem( Icons.crop_free, 'Cadrez la cible entière dans l\'image', ), _buildGuideItem(Icons.wb_sunny, 'Utilisez un bon éclairage'), _buildGuideItem(Icons.straighten, 'Prenez la photo de face'), _buildGuideItem(Icons.blur_off, 'Évitez les images floues'), _buildGuideItem( Icons.cleaning_services, 'Nettoyer votre objectif avec une chiffonnette', ), ], ), ), ); } Widget _buildGuideItem(IconData icon, String text) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( children: [ Icon(icon, size: 20, color: AppTheme.primaryColor), const SizedBox(width: 12), Expanded(child: Text(text)), ], ), ); } // Capture depuis la galerie d'images Future _captureImageFromGallery() async { setState(() => _isLoading = true); try { final XFile? image = await _picker.pickImage( source: ImageSource.gallery, maxWidth: 2048, maxHeight: 2048, imageQuality: 90, ); if (image != null) { setState(() { _selectedImagePath = image.path; }); _analyzeImage(); } } catch (e) { debugPrint('Erreur galerie: $e'); } finally { if (mounted) setState(() => _isLoading = false); } } void _analyzeImage() { if (_selectedImagePath == null) return; Navigator.push( context, MaterialPageRoute( builder: (_) => CropScreen( imagePath: _selectedImagePath!, targetType: _selectedType, ), ), ); } }