diff --git a/lib/features/analysis/analysis_screen.dart b/lib/features/analysis/analysis_screen.dart index 5484b224..80a423bd 100644 --- a/lib/features/analysis/analysis_screen.dart +++ b/lib/features/analysis/analysis_screen.dart @@ -1,8 +1,7 @@ -/// Écran principal d'analyse - Interface centrale de traitement des cibles. +/// Écran principal de Plotting et d'analyse - Interface centrale de traitement des cibles. /// -/// Affiche la cible avec overlay des anneaux et impacts détectés. -/// Permet la calibration, l'ajout manuel d'impacts, la détection automatique, -/// et le calcul des scores et statistiques de groupement. +/// Affiche d'abord la calibration de la cible, puis l'overlay des anneaux et impacts détectés. +/// Permet le calcul des scores et statistiques de groupement (Plotting). library; import 'dart:io'; @@ -16,7 +15,6 @@ import '../../data/repositories/session_repository.dart'; import '../../services/target_detection_service.dart'; import '../../services/score_calculator_service.dart'; import '../../services/grouping_analyzer_service.dart'; -import '../../services/wallet_identity_service.dart'; import '../session/session_provider.dart'; import 'analysis_provider.dart'; import '../crop/crop_screen.dart'; @@ -29,7 +27,9 @@ import 'widgets/grouping_stats.dart'; class AnalysisScreen extends StatelessWidget { final String imagePath; final TargetType targetType; - final Offset? targetCenter; + // CORRECTION : Utilisation de coordonnées directes pour effacer l'erreur rouge + final double? initialCenterX; + final double? initialCenterY; final double? cropScale; final Offset? cropOffset; @@ -37,20 +37,26 @@ class AnalysisScreen extends StatelessWidget { super.key, required this.imagePath, required this.targetType, - this.targetCenter, + this.initialCenterX, + this.initialCenterY, this.cropScale, this.cropOffset, }); @override Widget build(BuildContext context) { + // Reconstitution de l'Offset si présent pour le provider en arrière-plan + final manualCenterOffset = (initialCenterX != null && initialCenterY != null) + ? Offset(initialCenterX!, initialCenterY!) + : null; + return ChangeNotifierProvider( create: (context) => AnalysisProvider( detectionService: context.read(), scoreCalculatorService: context.read(), groupingAnalyzerService: context.read(), sessionRepository: context.read(), - )..analyzeImage(imagePath, targetType, autoAnalyze: false, manualCenter: targetCenter), + )..analyzeImage(imagePath, targetType, autoAnalyze: false, manualCenter: manualCenterOffset), child: _AnalysisScreenContent( cropScale: cropScale, cropOffset: cropOffset, @@ -74,13 +80,14 @@ class _AnalysisScreenContent extends StatefulWidget { class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { final GlobalKey _calibrationKey = GlobalKey(); - bool _isCalibrating = false; + + // CORRECTION : Forcé à TRUE pour arriver directement sur la calibration après le Crop ! + bool _isCalibrating = true; + bool _isSelectingReferences = false; - bool _isFullscreenEditMode = false; bool _isAtBottom = false; final ScrollController _scrollController = ScrollController(); - final TransformationController _transformationController = - TransformationController(); + final TransformationController _transformationController = TransformationController(); final GlobalKey _imageKey = GlobalKey(); double _currentZoomScale = 1.0; @@ -93,10 +100,9 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { void _onScroll() { if (!_scrollController.hasClients) return; - // Detect if we are near the bottom (within 20 pixels of the specific spacing we added) final isBottom = _scrollController.position.pixels >= - _scrollController.position.maxScrollExtent - 20; + _scrollController.position.maxScrollExtent - 20; if (isBottom != _isAtBottom) { setState(() { _isAtBottom = isBottom; @@ -127,8 +133,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { final provider = context.watch(); final sessionProvider = context.watch(); final targetNumber = sessionProvider.isSessionActive ? sessionProvider.targetCount + 1 : null; - final titlePrefix = _isCalibrating ? 'Calibration' : 'Analyse'; - final title = targetNumber != null ? '$titlePrefix - Cible $targetNumber' : ( _isCalibrating ? 'Calibration' : 'Analyse de Tir'); + + // CORRECTION : Remplacement du mot "Analyse" par "Plotting" + final titlePrefix = _isCalibrating ? 'Calibration' : 'Plotting'; + final title = targetNumber != null ? '$titlePrefix - Cible $targetNumber' : ( _isCalibrating ? 'Calibration' : 'Plotting du Tir'); return Scaffold( appBar: AppBar( @@ -137,26 +145,22 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { icon: const Icon(Icons.arrow_back), onPressed: () { if (_isCalibrating) { - setState(() => _isCalibrating = false); - } else if (_isSelectingReferences) { - setState(() => _isSelectingReferences = false); - } else { - // Return to crop screen instead of popping to capture + // Si on fait retour pendant la calibration, on retourne sagement au CropScreen final provider = context.read(); - final path = provider.imagePath!; - final type = provider.targetType!; - Navigator.pushReplacement( context, MaterialPageRoute( builder: (context) => CropScreen( - imagePath: path, - targetType: type, + imagePath: provider.imagePath!, + targetType: provider.targetType!, initialScale: widget.cropScale, initialOffset: widget.cropOffset, ), ), ); + } else { + // Si on fait retour depuis le Plotting, on revient en arrière dans la calibration + setState(() => _isCalibrating = true); } }, ), @@ -168,10 +172,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ), if (_isCalibrating) TextButton( - onPressed: () => setState(() => _isCalibrating = false), + onPressed: () => setState(() => _isCalibrating = false), // Dévoile le Plotting Screen child: const Text( 'TERMINER', - style: TextStyle(color: Colors.white), + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), ), ], @@ -182,7 +186,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { controller: _scrollController, child: Column( children: [ - // Session info header + // Header de chargement Padding( padding: const EdgeInsets.all(AppConstants.defaultPadding), child: Column( @@ -199,57 +203,57 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ), ), - // Target image with overlay or calibration + // Zone centrale d'affichage : Calibration OU Plotting interactif AspectRatio( aspectRatio: provider.imageAspectRatio, child: _isCalibrating ? Stack( - fit: StackFit.expand, - children: [ - Image.file( - File(provider.imagePath!), - fit: BoxFit.fill, - ), - TargetCalibration( - key: _calibrationKey, - initialCenterX: provider.targetCenterX, - initialCenterY: provider.targetCenterY, - initialRadius: provider.targetRadius, - initialInnerRadius: provider.targetInnerRadius, - initialRingCount: provider.ringCount, - initialRingRadii: provider.ringRadii, - targetType: provider.targetType!, - onCalibrationChanged: - ( - centerX, - centerY, - innerRadius, - radius, - ringCount, { - List? ringRadii, - }) { - provider.adjustTargetPosition( - centerX, - centerY, - innerRadius, - radius, - ringCount: ringCount, - ringRadii: ringRadii, - ); - }, - ), - ], - ) + fit: StackFit.expand, + children: [ + Image.file( + File(provider.imagePath!), + fit: BoxFit.fill, + ), + TargetCalibration( + key: _calibrationKey, + initialCenterX: provider.targetCenterX, + initialCenterY: provider.targetCenterY, + initialRadius: provider.targetRadius, + initialInnerRadius: provider.targetInnerRadius, + initialRingCount: provider.ringCount, + initialRingRadii: provider.ringRadii, + targetType: provider.targetType!, + onCalibrationChanged: + ( + centerX, + centerY, + innerRadius, + radius, + ringCount, { + List? ringRadii, + }) { + provider.adjustTargetPosition( + centerX, + centerY, + innerRadius, + radius, + ringCount: ringCount, + ringRadii: ringRadii, + ); + }, + ), + ], + ) : _buildZoomableImageWithOverlay(context, provider), ), - // Info cards or Calibration info + // Vue inférieure : Cartes de scores (Plotting) OU Flèches de réglages (Calibration) if (!_isCalibrating) Padding( padding: const EdgeInsets.all(AppConstants.defaultPadding), child: Column( children: [ - // Calibration button + // Petit bouton pour réajuster la calibration si besoin Card( color: AppTheme.primaryColor.withValues(alpha: 0.1), child: ListTile( @@ -257,9 +261,9 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { Icons.tune, color: AppTheme.primaryColor, ), - title: const Text('Calibrer la cible'), + title: const Text('Ajuster la calibration'), subtitle: const Text( - 'Ajustez le centre et la taille', + 'Modifier le centre ou le rayon global', ), trailing: const Icon( Icons.arrow_forward_ios, @@ -289,21 +293,17 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ), const SizedBox(height: 12), - - // Action buttons _buildActionButtons(context, provider), - const SizedBox(height: 50), ], ), ) else - // Calibration info WITH directional arrows ABOVE the card + // Mode Calibration actif : On affiche les flèches de micro-ajustement Padding( padding: const EdgeInsets.all(AppConstants.defaultPadding), child: Column( children: [ - // Fleches de micro-ajustement AU DESSUS du bloc const Text( 'Ajustement precis (pixel par pixel)', style: TextStyle( @@ -315,15 +315,15 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { const SizedBox(height: 8), Center( child: Builder( - builder: (context) { - final size = MediaQuery.of(context).size; - return _calibrationKey.currentState?.buildDirectionalControls(context, size) - ?? const SizedBox.shrink(); - } + builder: (context) { + final size = MediaQuery.of(context).size; + return _calibrationKey.currentState?.buildDirectionalControls(context, size) + ?? const SizedBox.shrink(); + } ), ), const SizedBox(height: 16), - + Card( child: Padding( padding: const EdgeInsets.all(16), @@ -340,15 +340,15 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { const SizedBox(height: 12), _buildInstructionItem( Icons.open_with, - 'Glissez le centre (croix bleue) pour positionner le centre de la cible', + 'Glissez le centre pour positionner le centre de la cible', ), _buildInstructionItem( Icons.zoom_out_map, - 'Glissez le bord (cercle orange) pour ajuster la taille de la cible', + 'Pincez l\'ecran a 2 doigts ou utilisez la jauge pour la taille', ), _buildInstructionItem( Icons.visibility, - 'Les zones de score sont affichees en transparence', + 'Appuyez sur TERMINER en haut a droite pour valider', ), const SizedBox(height: 16), Row( @@ -356,9 +356,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { const Text('Centre: '), Text( '(${(provider.targetCenterX * 100).toStringAsFixed(1)}%, ${(provider.targetCenterY * 100).toStringAsFixed(1)}%)', - style: const TextStyle( - fontWeight: FontWeight.bold, - ), + style: const TextStyle(fontWeight: FontWeight.bold), ), ], ), @@ -367,9 +365,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { const Text('Rayon: '), Text( '${(provider.targetRadius * 100).toStringAsFixed(1)}%', - style: const TextStyle( - fontWeight: FontWeight.bold, - ), + style: const TextStyle(fontWeight: FontWeight.bold), ), ], ), @@ -398,11 +394,11 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { child: _isCalibrating ? const SizedBox.shrink() : FloatingActionButton.extended( - onPressed: () => _showSaveSessionDialog(context, provider), - backgroundColor: AppTheme.primaryColor, - icon: const Icon(Icons.save), - label: const Text('TERMINER LA SESSION'), - ), + onPressed: () => _showSaveSessionDialog(context, provider), + backgroundColor: AppTheme.primaryColor, + icon: const Icon(Icons.save), + label: const Text('TERMINER LA SESSION'), + ), ), ), ), @@ -430,9 +426,9 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { } Widget _buildZoomableImageWithOverlay( - BuildContext context, - AnalysisProvider provider, - ) { + BuildContext context, + AnalysisProvider provider, + ) { return InteractiveViewer( transformationController: _transformationController, minScale: 1.0, @@ -462,42 +458,20 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { } Widget _buildActionButtons(BuildContext context, AnalysisProvider provider) { - return Column( + return const Column( children: [ Row( - children: [ - /*Expanded( - child: ElevatedButton.icon( - onPressed: null, // Desactive selon demande - icon: const Icon(Icons.auto_awesome), - label: const Text('AUTO DÉTECTER'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.grey, - ), - ), - ), - const SizedBox(width: 12), - Expanded( - child: ElevatedButton.icon( - onPressed: null, // Desactive selon demande - icon: const Icon(Icons.straighten), - label: const Text('PAR RÉFÉRENCE'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.grey, - ), - ), - ),*/ - ], + children: [], ), ], ); } void _showShotDetails( - BuildContext context, - AnalysisProvider provider, - Shot shot, - ) { + BuildContext context, + AnalysisProvider provider, + Shot shot, + ) { showModalBottomSheet( context: context, builder: (context) => Container( @@ -521,14 +495,14 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { value: shot.score.clamp(0, 10), items: List.generate(11, (index) => index) .map((s) => DropdownMenuItem( - value: s, - child: Text('$s', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), - )) + value: s, + child: Text('$s', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), + )) .toList(), onChanged: (newScore) { if (newScore != null) { provider.updateShotScore(shot.id, newScore); - Navigator.pop(context); // Ferme pour valider + Navigator.pop(context); } }, ), @@ -555,8 +529,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { } void _showSaveSessionDialog(BuildContext context, AnalysisProvider provider) { - final sessionProvider = context.read(); - showDialog( context: context, builder: (context) => AlertDialog( @@ -574,11 +546,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { actions: [ TextButton( onPressed: () { - Navigator.pop(context); // Close dialog - + Navigator.pop(context); final path = provider.imagePath!; final type = provider.targetType!; - + Navigator.pushReplacement( context, MaterialPageRoute( @@ -603,17 +574,15 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { weaponId: sessionProvider.currentWeaponId, distance: sessionProvider.distance, ); - - // Mettre à jour le provider de session pour incrémenter le compteur + sessionProvider.addAnalysis(analysis); if (context.mounted) { - Navigator.pop(context); // Close dialog - // Go back to capture screen for a new target in the same session context + Navigator.pop(context); Navigator.pushAndRemoveUntil( context, MaterialPageRoute(builder: (context) => const CaptureScreen()), - (route) => route.isFirst, + (route) => route.isFirst, ); } } catch (e) { @@ -636,13 +605,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { weaponId: sessionProvider.currentWeaponId, distance: sessionProvider.distance, ); - + if (context.mounted) { - // Important: Marquer la session comme terminée dans le provider global sessionProvider.endSession(); - - Navigator.pop(context); // Ferme le dialogue - // Retourne à l'accueil (premier écran) + Navigator.pop(context); Navigator.of(context).popUntil((route) => route.isFirst); } } catch (e) { @@ -659,4 +625,4 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ), ); } -} +} \ No newline at end of file diff --git a/lib/features/crop/crop_screen.dart b/lib/features/crop/crop_screen.dart index ab37f991..b9453469 100644 --- a/lib/features/crop/crop_screen.dart +++ b/lib/features/crop/crop_screen.dart @@ -250,22 +250,22 @@ class _CropScreenState extends State { setState(() => _isLoading = true); try { final cropRect = _calculateCropRect(); - // On calcule le centre relatif basé sur le centrage utilisateur final targetCenterX = cropRect.x + cropRect.width / 2; final targetCenterY = cropRect.y + cropRect.height / 2; if (!mounted) return; - // Note : On laisse temporairement AnalysisScreen ici. - // Dès qu'on s'attaque au fichier de calibration, on modifiera cette ligne - // pour basculer directement sur le bon écran ! + // CHANGEMENT DE FLUX : On saute l'analyse et on va DIRECTEMENT calibrer la cible ! + // On passe à l'écran d'analyse (qui va être renommé PlottingScreen) toutes les infos requises. Navigator.pushReplacement( context, MaterialPageRoute( builder: (_) => AnalysisScreen( imagePath: widget.imagePath, targetType: widget.targetType, - targetCenter: Offset(targetCenterX, targetCenterY), + // On initialise le centre avec le point que l'utilisateur vient de cibler + initialCenterX: targetCenterX, + initialCenterY: targetCenterY, cropScale: _scale, cropOffset: _offset, ),