From 653264c6a114f6492fa7e6993b260d12c2736f32 Mon Sep 17 00:00:00 2001 From: qguillaume Date: Tue, 26 May 2026 18:09:39 +0200 Subject: [PATCH] =?UTF-8?q?Refactor:=20Am=C3=A9lioration=20globale=20de=20?= =?UTF-8?q?l'exp=C3=A9rience=20de=20cadrage=20et=20de=20l'interface=20de?= =?UTF-8?q?=20capture?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/features/capture/capture_screen.dart | 303 +++++++++++++++++++++-- pubspec.lock | 74 +++++- pubspec.yaml | 1 + 3 files changed, 340 insertions(+), 38 deletions(-) diff --git a/lib/features/capture/capture_screen.dart b/lib/features/capture/capture_screen.dart index 8a2780cb..bc1979cf 100644 --- a/lib/features/capture/capture_screen.dart +++ b/lib/features/capture/capture_screen.dart @@ -1,8 +1,10 @@ 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'; @@ -19,12 +21,78 @@ class CaptureScreen extends StatefulWidget { State createState() => _CaptureScreenState(); } -class _CaptureScreenState extends State { +class _CaptureScreenState extends State with SingleTickerProviderStateMixin { final ImagePicker _picker = ImagePicker(); 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.high, + enableAudio: false, + ); + + await _cameraController!.initialize(); + setState(() { + _isCameraInitialized = true; + _showLiveCamera = true; + }); + } + } catch (e) { + debugPrint('Erreur caméra: $e'); + } finally { + setState(() => _isLoading = false); + } + } + Future _handleGallerySelection() async { PermissionStatus status; @@ -42,7 +110,7 @@ class _CaptureScreenState extends State { } if (status.isGranted) { - _captureImage(ImageSource.gallery); + _captureImageFromGallery(); } else if (status.isPermanentlyDenied) { _showSettingsDialog(); } else { @@ -85,6 +153,11 @@ class _CaptureScreenState extends State { ? '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( @@ -102,8 +175,7 @@ class _CaptureScreenState extends State { child: ImageSourceButton( icon: Icons.camera_alt, label: 'Scanner', - // Appelle maintenant l'appareil photo classique - onPressed: _isLoading ? null : _openStandardCamera, + onPressed: _isLoading ? null : _initLiveCamera, // Ouvre notre caméra intégrée ), ), const SizedBox(width: 12), @@ -133,6 +205,202 @@ class _CaptureScreenState extends State { ); } + // 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 + 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 { + // Prise de la photo + final XFile photo = await _cameraController!.takePicture(); + + setState(() { + _selectedImagePath = photo.path; + _showLiveCamera = false; // Quitte la vue caméra + }); + + // Redirection automatique vers l'écran de Crop + _analyzeImage(); + } catch (e) { + debugPrint('Erreur lors du clic photo: $e'); + } finally { + setState(() => _isLoading = false); + } + } + Widget _buildSectionTitle(String title) { return Text( title, @@ -187,25 +455,12 @@ class _CaptureScreenState extends State { ); } - // NOUVELLE FONCTION : Ouvre l'appareil photo classique 100% Manuel - Future _openStandardCamera() async { - final status = await Permission.camera.request(); - if (status.isGranted) { - _captureImage(ImageSource.camera); - } else { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Accès à l\'appareil photo requis')), - ); - } - } - } - - Future _captureImage(ImageSource source) async { + // Capture depuis la galerie d'images + Future _captureImageFromGallery() async { setState(() => _isLoading = true); try { final XFile? image = await _picker.pickImage( - source: source, + source: ImageSource.gallery, maxWidth: 2048, maxHeight: 2048, imageQuality: 90, @@ -214,20 +469,18 @@ class _CaptureScreenState extends State { setState(() { _selectedImagePath = image.path; }); - // On appelle directement l'analyse avec le bon chemin de fichier _analyzeImage(); } } catch (e) { - debugPrint('Erreur capture: $e'); + debugPrint('Erreur galerie: $e'); } finally { - if (mounted) setState(() => _isLoading = false); - } + if (mounted) setState(() => _isLoading = false); + } } void _analyzeImage() { if (_selectedImagePath == null) return; - // Correction de la redirection pour ouvrir proprement le CropScreen Navigator.push( context, MaterialPageRoute( diff --git a/pubspec.lock b/pubspec.lock index 6c85ab31..40771b10 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,6 +33,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + camera: + dependency: "direct main" + description: + name: camera + sha256: "034c38cb8014d29698dcae6d20276688a1bf74e6487dfeb274d70ea05d5f7777" + url: "https://pub.dev" + source: hosted + version: "0.12.0+1" + camera_android_camerax: + dependency: transitive + description: + name: camera_android_camerax + sha256: b5064cf25a2787d122d0bf12e77c7b1033a2b983d0730e3091f770ee376efde5 + url: "https://pub.dev" + source: hosted + version: "0.7.2" + camera_avfoundation: + dependency: transitive + description: + name: camera_avfoundation + sha256: "90e4cc3fde331581a3b2d35d83be41dbb7393af0ab857eb27b732174289cb96d" + url: "https://pub.dev" + source: hosted + version: "0.10.1" + camera_platform_interface: + dependency: transitive + description: + name: camera_platform_interface + sha256: "7ac852d77699acee79f0d438b793feee26721841e50973576419ff5c6d95e9b7" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + camera_web: + dependency: transitive + description: + name: camera_web + sha256: "57f49a635c8bf249d07fb95eb693d7e4dda6796dedb3777f9127fb54847beba7" + url: "https://pub.dev" + source: hosted + version: "0.3.5+3" change_case: dependency: transitive description: @@ -45,10 +85,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" clock: dependency: transitive description: @@ -425,26 +465,26 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.0" mime: dependency: transitive description: @@ -802,6 +842,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" string_scanner: dependency: transitive description: @@ -814,10 +862,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5" + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 url: "https://pub.dev" source: hosted - version: "3.4.0+1" + version: "3.4.0" term_glyph: dependency: transitive description: @@ -830,10 +878,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.7" typed_data: dependency: transitive description: @@ -915,5 +963,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.11.0 <4.0.0" + dart: ">=3.10.3 <4.0.0" flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml index b9fa0bd0..0a19a2fe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,6 +70,7 @@ dependencies: device_info_plus: ^13.1.0 shared_preferences: ^2.5.5 crypto: ^3.0.7 + camera: ^0.12.0+1 # Machine Learning for YOLOv8 # tflite_flutter: ^0.11.0