diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/features/analysis/analysis_provider.dart b/lib/features/analysis/analysis_provider.dart index 748a378..3ab4cc1 100644 --- a/lib/features/analysis/analysis_provider.dart +++ b/lib/features/analysis/analysis_provider.dart @@ -34,11 +34,11 @@ class AnalysisProvider extends ChangeNotifier { required GroupingAnalyzerService groupingAnalyzerService, required SessionRepository sessionRepository, DistortionCorrectionService? distortionService, - }) : _detectionService = detectionService, - _scoreCalculatorService = scoreCalculatorService, - _groupingAnalyzerService = groupingAnalyzerService, - _sessionRepository = sessionRepository, - _distortionService = distortionService ?? DistortionCorrectionService(); + }) : _detectionService = detectionService, + _scoreCalculatorService = scoreCalculatorService, + _groupingAnalyzerService = groupingAnalyzerService, + _sessionRepository = sessionRepository, + _distortionService = distortionService ?? DistortionCorrectionService(); AnalysisState _state = AnalysisState.initial; String? _errorMessage; @@ -80,7 +80,8 @@ class AnalysisProvider extends ChangeNotifier { double get targetCenterY => _targetCenterY; double get targetRadius => _targetRadius; int get ringCount => _ringCount; - List? get ringRadii => _ringRadii != null ? List.unmodifiable(_ringRadii!) : null; + List? get ringRadii => + _ringRadii != null ? List.unmodifiable(_ringRadii!) : null; double get imageAspectRatio => _imageAspectRatio; List get shots => List.unmodifiable(_shots); ScoreResult? get scoreResult => _scoreResult; @@ -97,13 +98,22 @@ class AnalysisProvider extends ChangeNotifier { DistortionParameters? get distortionParams => _distortionParams; String? get correctedImagePath => _correctedImagePath; bool get hasDistortion => _distortionParams?.needsCorrection ?? false; + /// Retourne le chemin de l'image à afficher (corrigée si activée, originale sinon) - String? get displayImagePath => _distortionCorrectionEnabled && _correctedImagePath != null + String? get displayImagePath => + _distortionCorrectionEnabled && _correctedImagePath != null ? _correctedImagePath : _imagePath; /// Analyze an image - Future analyzeImage(String imagePath, TargetType targetType) async { + /// + /// [autoAnalyze] determines if we should run automatic detection immediately. + /// If false, only the image is loaded and default target parameters are set. + Future analyzeImage( + String imagePath, + TargetType targetType, { + bool autoAnalyze = true, + }) async { _state = AnalysisState.loading; _imagePath = imagePath; _targetType = targetType; @@ -119,6 +129,20 @@ class AnalysisProvider extends ChangeNotifier { _imageAspectRatio = frame.image.width / frame.image.height; frame.image.dispose(); + if (!autoAnalyze) { + // Just setup default values without running detection + _targetCenterX = 0.5; + _targetCenterY = 0.5; + _targetRadius = 0.4; + + // Initialize empty shots list + _shots = []; + + _state = AnalysisState.success; + notifyListeners(); + return; + } + // Detect target and impacts final result = _detectionService.detectTarget(imagePath, targetType); @@ -162,13 +186,7 @@ class AnalysisProvider extends ChangeNotifier { /// Add a manual shot void addShot(double x, double y) { final score = _calculateShotScore(x, y); - final shot = Shot( - id: _uuid.v4(), - x: x, - y: y, - score: score, - sessionId: '', - ); + final shot = Shot(id: _uuid.v4(), x: x, y: y, score: score, sessionId: ''); _shots.add(shot); _recalculateScores(); @@ -190,11 +208,7 @@ class AnalysisProvider extends ChangeNotifier { if (index == -1) return; final newScore = _calculateShotScore(newX, newY); - _shots[index] = _shots[index].copyWith( - x: newX, - y: newY, - score: newScore, - ); + _shots[index] = _shots[index].copyWith(x: newX, y: newY, score: newScore); _recalculateScores(); _recalculateGrouping(); @@ -334,7 +348,9 @@ class AnalysisProvider extends ChangeNotifier { double tolerance = 2.0, bool clearExisting = false, }) async { - if (_imagePath == null || _targetType == null || _referenceImpacts.length < 2) { + if (_imagePath == null || + _targetType == null || + _referenceImpacts.length < 2) { return 0; } @@ -343,16 +359,17 @@ class AnalysisProvider extends ChangeNotifier { .map((shot) => ReferenceImpact(x: shot.x, y: shot.y)) .toList(); - final detectedImpacts = _detectionService.detectImpactsWithOpenCVFromReferences( - _imagePath!, - _targetType!, - _targetCenterX, - _targetCenterY, - _targetRadius, - _ringCount, - references, - tolerance: tolerance, - ); + final detectedImpacts = _detectionService + .detectImpactsWithOpenCVFromReferences( + _imagePath!, + _targetType!, + _targetCenterX, + _targetCenterY, + _targetRadius, + _ringCount, + references, + tolerance: tolerance, + ); if (clearExisting) { _shots.clear(); @@ -381,13 +398,7 @@ class AnalysisProvider extends ChangeNotifier { /// Add a reference impact for calibrated detection void addReferenceImpact(double x, double y) { final score = _calculateShotScore(x, y); - final shot = Shot( - id: _uuid.v4(), - x: x, - y: y, - score: score, - sessionId: '', - ); + final shot = Shot(id: _uuid.v4(), x: x, y: y, score: score, sessionId: ''); _referenceImpacts.add(shot); notifyListeners(); } @@ -428,7 +439,9 @@ class AnalysisProvider extends ChangeNotifier { double tolerance = 2.0, bool clearExisting = false, }) async { - if (_imagePath == null || _targetType == null || _learnedCharacteristics == null) { + if (_imagePath == null || + _targetType == null || + _learnedCharacteristics == null) { return 0; } @@ -468,7 +481,13 @@ class AnalysisProvider extends ChangeNotifier { } /// Adjust target position - void adjustTargetPosition(double centerX, double centerY, double radius, {int? ringCount, List? ringRadii}) { + void adjustTargetPosition( + double centerX, + double centerY, + double radius, { + int? ringCount, + List? ringRadii, + }) { _targetCenterX = centerX; _targetCenterY = centerY; _targetRadius = radius; @@ -529,8 +548,7 @@ class AnalysisProvider extends ChangeNotifier { } } - -/* version deux a tester*/ + /* version deux a tester*/ /// Calcule ET applique la correction pour un feedback immédiat Future calculateAndApplyDistortion() async { // 1. Calcul des paramètres (votre code actuel) @@ -540,11 +558,11 @@ class AnalysisProvider extends ChangeNotifier { targetRadius: _targetRadius, imageAspectRatio: _imageAspectRatio, ); - + // 2. Vérification si une correction est réellement nécessaire if (_distortionParams != null && _distortionParams!.needsCorrection) { // 3. Application immédiate de la transformation (méthode asynchrone) - await applyDistortionCorrection(); + await applyDistortionCorrection(); } else { notifyListeners(); // On prévient quand même si pas de correction } @@ -566,7 +584,7 @@ class AnalysisProvider extends ChangeNotifier { notifyListeners(); } } -/* fin section deux a tester*/ + /* fin section deux a tester*/ int _calculateShotScore(double x, double y) { if (_targetType == TargetType.concentric) { diff --git a/lib/features/analysis/analysis_screen.dart b/lib/features/analysis/analysis_screen.dart index 486fa82..e1d24dd 100644 --- a/lib/features/analysis/analysis_screen.dart +++ b/lib/features/analysis/analysis_screen.dart @@ -39,7 +39,7 @@ class AnalysisScreen extends StatelessWidget { scoreCalculatorService: context.read(), groupingAnalyzerService: context.read(), sessionRepository: context.read(), - )..analyzeImage(imagePath, targetType), + )..analyzeImage(imagePath, targetType, autoAnalyze: false), child: const _AnalysisScreenContent(), ); } @@ -58,7 +58,8 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { 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; @@ -72,7 +73,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; + final isBottom = + _scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 20; if (isBottom != _isAtBottom) { setState(() { _isAtBottom = isBottom; @@ -115,13 +118,16 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { actions: [ Consumer( builder: (context, provider, _) { - if (provider.state != AnalysisState.success) return const SizedBox.shrink(); + if (provider.state != AnalysisState.success) + return const SizedBox.shrink(); return IconButton( icon: Icon(_isCalibrating ? Icons.check : Icons.tune), onPressed: () { setState(() => _isCalibrating = !_isCalibrating); }, - tooltip: _isCalibrating ? 'Terminer calibration' : 'Calibrer la cible', + tooltip: _isCalibrating + ? 'Terminer calibration' + : 'Calibrer la cible', color: _isCalibrating ? AppTheme.successColor : null, ); }, @@ -155,7 +161,11 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.error_outline, size: 64, color: AppTheme.errorColor), + Icon( + Icons.error_outline, + size: 64, + color: AppTheme.errorColor, + ), const SizedBox(height: 16), Text( provider.errorMessage ?? 'Une erreur est survenue', @@ -189,388 +199,508 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { SingleChildScrollView( controller: _scrollController, child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Calibration mode indicator - if (_isCalibrating) - Container( - color: AppTheme.warningColor, - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - child: const Row( - children: [ - Icon(Icons.tune, color: Colors.white, size: 20), - SizedBox(width: 8), - Expanded( - child: Text( - 'Mode Calibration - Ajustez le centre et la taille de la cible', - style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), - ), + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Calibration mode indicator + if (_isCalibrating) + Container( + color: AppTheme.warningColor, + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, ), - ], - ), - ), - - // Reference selection mode indicator - if (_isSelectingReferences) - Container( - color: Colors.deepPurple, - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - child: Row( - children: [ - const Icon(Icons.touch_app, color: Colors.white, size: 20), - const SizedBox(width: 8), - Expanded( - child: Text( - 'Selectionnez ${provider.referenceImpacts.length}/3-4 impacts de reference', - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), - ), - ), - if (provider.referenceImpacts.isNotEmpty) - TextButton( - onPressed: () => provider.clearReferenceImpacts(), - child: const Text('Effacer', style: TextStyle(color: Colors.white)), - ), - ], - ), - ), - - // Calibration sliders (shown only in calibration mode) - if (_isCalibrating) - Container( - color: Colors.black87, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Column( - children: [ - // Ring count slider - Row( + child: const Row( children: [ - const Icon(Icons.radio_button_unchecked, color: Colors.white, size: 20), - const SizedBox(width: 8), - const Text('Anneaux:', style: TextStyle(color: Colors.white)), + Icon(Icons.tune, color: Colors.white, size: 20), + SizedBox(width: 8), Expanded( - child: Slider( - value: provider.ringCount.toDouble(), - min: 3, - max: 12, - divisions: 9, - label: '${provider.ringCount}', - activeColor: AppTheme.primaryColor, - onChanged: (value) { - provider.adjustTargetPosition( - provider.targetCenterX, - provider.targetCenterY, - provider.targetRadius, - ringCount: value.round(), - ); - }, - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - decoration: BoxDecoration( - color: AppTheme.primaryColor, - borderRadius: BorderRadius.circular(12), - ), child: Text( - '${provider.ringCount}', - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), - ), - ), - ], - ), - // Target size slider - Row( - children: [ - const Icon(Icons.zoom_out_map, color: Colors.white, size: 20), - const SizedBox(width: 8), - const Text('Taille:', style: TextStyle(color: Colors.white)), - Expanded( - child: Slider( - value: provider.targetRadius.clamp(0.05, 3.0), - min: 0.05, - max: 3.0, - label: '${(provider.targetRadius * 100).toStringAsFixed(0)}%', - activeColor: AppTheme.warningColor, - onChanged: (value) { - provider.adjustTargetPosition( - provider.targetCenterX, - provider.targetCenterY, - value, - ringCount: provider.ringCount, - ); - }, - ), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - decoration: BoxDecoration( - color: AppTheme.warningColor, - borderRadius: BorderRadius.circular(12), - ), - child: Text( - '${(provider.targetRadius * 100).toStringAsFixed(0)}%', - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), - ), - ), - ], - ), - const Divider(color: Colors.white24, height: 16), - // Distortion correction row - Row( - children: [ - const Icon(Icons.lens_blur, color: Colors.white, size: 20), - const SizedBox(width: 8), - const Expanded( - child: Text('Correction distorsion:', style: TextStyle(color: Colors.white)), - ), - if (provider.distortionParams == null) - ElevatedButton.icon( - onPressed: () { - provider.calculateDistortion(); - }, - icon: const Icon(Icons.calculate, size: 16), - label: const Text('Calculer'), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.blueGrey, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + 'Mode Calibration - Ajustez le centre et la taille de la cible', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, ), - ) - else ...[ - if (provider.correctedImagePath == null) - ElevatedButton.icon( - onPressed: () { - provider.applyDistortionCorrection(); - }, - icon: const Icon(Icons.auto_fix_high, size: 16), - label: const Text('Appliquer'), - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.primaryColor, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + ), + ), + ], + ), + ), + + // Reference selection mode indicator + if (_isSelectingReferences) + Container( + color: Colors.deepPurple, + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + child: Row( + children: [ + const Icon( + Icons.touch_app, + color: Colors.white, + size: 20, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Selectionnez ${provider.referenceImpacts.length}/3-4 impacts de reference', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + if (provider.referenceImpacts.isNotEmpty) + TextButton( + onPressed: () => provider.clearReferenceImpacts(), + child: const Text( + 'Effacer', + style: TextStyle(color: Colors.white), + ), + ), + ], + ), + ), + + // Calibration sliders (shown only in calibration mode) + if (_isCalibrating) + Container( + color: Colors.black87, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Column( + children: [ + // Ring count slider + Row( + children: [ + const Icon( + Icons.radio_button_unchecked, + color: Colors.white, + size: 20, + ), + const SizedBox(width: 8), + const Text( + 'Anneaux:', + style: TextStyle(color: Colors.white), + ), + Expanded( + child: Slider( + value: provider.ringCount.toDouble(), + min: 3, + max: 12, + divisions: 9, + label: '${provider.ringCount}', + activeColor: AppTheme.primaryColor, + onChanged: (value) { + provider.adjustTargetPosition( + provider.targetCenterX, + provider.targetCenterY, + provider.targetRadius, + ringCount: value.round(), + ); + }, ), - ) - else + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + decoration: BoxDecoration( + color: AppTheme.primaryColor, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '${provider.ringCount}', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + // Target size slider + Row( + children: [ + const Icon( + Icons.zoom_out_map, + color: Colors.white, + size: 20, + ), + const SizedBox(width: 8), + const Text( + 'Taille:', + style: TextStyle(color: Colors.white), + ), + Expanded( + child: Slider( + value: provider.targetRadius.clamp(0.05, 3.0), + min: 0.05, + max: 3.0, + label: + '${(provider.targetRadius * 100).toStringAsFixed(0)}%', + activeColor: AppTheme.warningColor, + onChanged: (value) { + provider.adjustTargetPosition( + provider.targetCenterX, + provider.targetCenterY, + value, + ringCount: provider.ringCount, + ); + }, + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + decoration: BoxDecoration( + color: AppTheme.warningColor, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '${(provider.targetRadius * 100).toStringAsFixed(0)}%', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const Divider(color: Colors.white24, height: 16), + // Distortion correction row + Row( + children: [ + const Icon( + Icons.lens_blur, + color: Colors.white, + size: 20, + ), + const SizedBox(width: 8), + const Expanded( + child: Text( + 'Correction distorsion:', + style: TextStyle(color: Colors.white), + ), + ), + if (provider.distortionParams == null) + ElevatedButton.icon( + onPressed: () { + provider.calculateDistortion(); + }, + icon: const Icon(Icons.calculate, size: 16), + label: const Text('Calculer'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueGrey, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + ), + ) + else ...[ + if (provider.correctedImagePath == null) + ElevatedButton.icon( + onPressed: () { + provider.applyDistortionCorrection(); + }, + icon: const Icon(Icons.auto_fix_high, size: 16), + label: const Text('Appliquer'), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + ), + ) + else + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.check_circle, + color: Colors.green, + size: 16, + ), + const SizedBox(width: 4), + const Text( + 'Corrigée', + style: TextStyle( + color: Colors.green, + fontSize: 12, + ), + ), + const SizedBox(width: 8), + Switch( + value: provider.distortionCorrectionEnabled, + onChanged: (value) => provider + .setDistortionCorrectionEnabled(value), + activeTrackColor: AppTheme.primaryColor + .withValues(alpha: 0.5), + activeThumbColor: AppTheme.primaryColor, + ), + ], + ), + ], + ], + ), + ], + ), + ), + + // Target image with overlay or calibration + AspectRatio( + aspectRatio: provider.imageAspectRatio, + child: _isCalibrating + ? Stack( + fit: StackFit.expand, + children: [ + Image.file( + File(provider.imagePath!), + fit: BoxFit.fill, + ), + TargetCalibration( + initialCenterX: provider.targetCenterX, + initialCenterY: provider.targetCenterY, + initialRadius: provider.targetRadius, + initialRingCount: provider.ringCount, + initialRingRadii: provider.ringRadii, + targetType: provider.targetType!, + onCalibrationChanged: + ( + centerX, + centerY, + radius, + ringCount, { + List? ringRadii, + }) { + provider.adjustTargetPosition( + centerX, + centerY, + radius, + ringCount: ringCount, + ringRadii: ringRadii, + ); + }, + ), + ], + ) + : _buildZoomableImageWithOverlay(context, provider), + ), + + // Info cards (hidden during calibration) + if (!_isCalibrating) + Padding( + padding: const EdgeInsets.all(AppConstants.defaultPadding), + child: Column( + children: [ + // Calibration button + Card( + color: AppTheme.primaryColor.withValues(alpha: 0.1), + child: ListTile( + leading: const Icon( + Icons.tune, + color: AppTheme.primaryColor, + ), + title: const Text('Calibrer la cible'), + subtitle: const Text( + 'Ajustez le centre et la taille', + ), + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + ), + onTap: () => setState(() => _isCalibrating = true), + ), + ), + const SizedBox(height: 12), + + // Score card + ScoreCard( + totalScore: provider.totalScore, + shotCount: provider.shotCount, + scoreResult: provider.scoreResult, + targetType: provider.targetType!, + ), + const SizedBox(height: 12), + + // Grouping stats + if (provider.groupingResult != null && + provider.shotCount > 1) + GroupingStats( + groupingResult: provider.groupingResult!, + targetCenterX: provider.targetCenterX, + targetCenterY: provider.targetCenterY, + ), + + const SizedBox(height: 12), + + // Action buttons + _buildActionButtons(context, provider), + + // Large spacing at the bottom to trigger the state change + const SizedBox(height: 50), + ], + ), + ) + else + // Calibration info + Padding( + padding: const EdgeInsets.all(AppConstants.defaultPadding), + child: Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Instructions de calibration', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(height: 12), + _buildInstructionItem( + Icons.open_with, + 'Glissez le centre (croix bleue) pour positionner le centre de la cible', + ), + _buildInstructionItem( + Icons.zoom_out_map, + 'Glissez le bord (cercle orange) pour ajuster la taille de la cible', + ), + _buildInstructionItem( + Icons.visibility, + 'Les zones de score sont affichees en transparence', + ), + const SizedBox(height: 12), Row( - mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.check_circle, color: Colors.green, size: 16), - const SizedBox(width: 4), - const Text('Corrigée', style: TextStyle(color: Colors.green, fontSize: 12)), - const SizedBox(width: 8), - Switch( - value: provider.distortionCorrectionEnabled, - onChanged: (value) => provider.setDistortionCorrectionEnabled(value), - activeTrackColor: AppTheme.primaryColor.withValues(alpha: 0.5), - activeThumbColor: AppTheme.primaryColor, + const Text('Centre: '), + Text( + '(${(provider.targetCenterX * 100).toStringAsFixed(1)}%, ${(provider.targetCenterY * 100).toStringAsFixed(1)}%)', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), ), ], ), - ], - ], + Row( + children: [ + const Text('Rayon: '), + Text( + '${(provider.targetRadius * 100).toStringAsFixed(1)}%', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ], + ), + ), ), - ], - ), - ), - - // Target image with overlay or calibration - AspectRatio( - aspectRatio: provider.imageAspectRatio, - child: _isCalibrating - ? Stack( - fit: StackFit.expand, - children: [ - Image.file( - File(provider.imagePath!), - fit: BoxFit.fill, - ), - TargetCalibration( - initialCenterX: provider.targetCenterX, - initialCenterY: provider.targetCenterY, - initialRadius: provider.targetRadius, - initialRingCount: provider.ringCount, - initialRingRadii: provider.ringRadii, - targetType: provider.targetType!, - onCalibrationChanged: (centerX, centerY, radius, ringCount, {List? ringRadii}) { - provider.adjustTargetPosition(centerX, centerY, radius, ringCount: ringCount, ringRadii: ringRadii); - }, - ), - ], - ) - : _buildZoomableImageWithOverlay(context, provider), + ), + ], ), - - // Info cards (hidden during calibration) - if (!_isCalibrating) - Padding( - padding: const EdgeInsets.all(AppConstants.defaultPadding), - child: Column( - children: [ - // Calibration button - Card( - color: AppTheme.primaryColor.withValues(alpha: 0.1), - child: ListTile( - leading: const Icon(Icons.tune, color: AppTheme.primaryColor), - title: const Text('Calibrer la cible'), - subtitle: const Text('Ajustez le centre et la taille'), - trailing: const Icon(Icons.arrow_forward_ios, size: 16), - onTap: () => setState(() => _isCalibrating = true), - ), - ), - const SizedBox(height: 12), - - // Score card - ScoreCard( - totalScore: provider.totalScore, - shotCount: provider.shotCount, - scoreResult: provider.scoreResult, - targetType: provider.targetType!, - ), - const SizedBox(height: 12), - - // Grouping stats - if (provider.groupingResult != null && provider.shotCount > 1) - GroupingStats( - groupingResult: provider.groupingResult!, - targetCenterX: provider.targetCenterX, - targetCenterY: provider.targetCenterY, - ), - - const SizedBox(height: 12), - - // Action buttons - _buildActionButtons(context, provider), - - // Large spacing at the bottom to trigger the state change - const SizedBox(height: 50), - ], - ), - ) - - else - // Calibration info - Padding( - padding: const EdgeInsets.all(AppConstants.defaultPadding), - child: Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Instructions de calibration', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), - const SizedBox(height: 12), - _buildInstructionItem( - Icons.open_with, - 'Glissez le centre (croix bleue) pour positionner le centre de la cible', - ), - _buildInstructionItem( - Icons.zoom_out_map, - 'Glissez le bord (cercle orange) pour ajuster la taille de la cible', - ), - _buildInstructionItem( - Icons.visibility, - 'Les zones de score sont affichees en transparence', - ), - const SizedBox(height: 12), - Row( - children: [ - const Text('Centre: '), - Text( - '(${(provider.targetCenterX * 100).toStringAsFixed(1)}%, ${(provider.targetCenterY * 100).toStringAsFixed(1)}%)', - style: const TextStyle(fontWeight: FontWeight.bold), - ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Align( + alignment: _isAtBottom + ? Alignment.bottomCenter + : Alignment.bottomRight, + child: Padding( + padding: _isAtBottom + ? EdgeInsets.zero + : const EdgeInsets.all(16.0), + child: _isCalibrating + ? FloatingActionButton.extended( + onPressed: () => setState(() => _isCalibrating = false), + backgroundColor: AppTheme.successColor, + icon: const Icon(Icons.check), + label: const Text('Valider'), + ) + : AnimatedContainer( + duration: const Duration(milliseconds: 260), + curve: Curves.easeInOut, + width: _isAtBottom + ? MediaQuery.of(context).size.width + : 180, + height: 56, + decoration: BoxDecoration( + color: AppTheme.primaryColor, + borderRadius: BorderRadius.circular( + _isAtBottom ? 0 : 16, + ), + boxShadow: [ + if (!_isAtBottom) + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 6, + offset: const Offset(0, 3), + ), ], ), - Row( - children: [ - const Text('Rayon: '), - Text( - '${(provider.targetRadius * 100).toStringAsFixed(1)}%', - style: const TextStyle(fontWeight: FontWeight.bold), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _saveSession(context, provider), + borderRadius: BorderRadius.circular( + _isAtBottom ? 0 : 16, ), - ], - ), - ], - ), - ), - ), - ), - ], - ), - ), - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Align( - alignment: _isAtBottom ? Alignment.bottomCenter : Alignment.bottomRight, - child: Padding( - padding: _isAtBottom ? EdgeInsets.zero : const EdgeInsets.all(16.0), - child: _isCalibrating - ? FloatingActionButton.extended( - onPressed: () => setState(() => _isCalibrating = false), - backgroundColor: AppTheme.successColor, - icon: const Icon(Icons.check), - label: const Text('Valider'), - ) - : AnimatedContainer( - duration: const Duration(milliseconds: 260), - curve: Curves.easeInOut, - width: _isAtBottom ? MediaQuery.of(context).size.width : 180, - height: 56, - decoration: BoxDecoration( - color: AppTheme.primaryColor, - borderRadius: BorderRadius.circular(_isAtBottom ? 0 : 16), - boxShadow: [ - if (!_isAtBottom) - BoxShadow( - color: Colors.black.withOpacity(0.2), - blurRadius: 6, - offset: const Offset(0, 3), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () => _saveSession(context, provider), - borderRadius: BorderRadius.circular(_isAtBottom ? 0 : 16), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - child: FittedBox( - fit: BoxFit.scaleDown, - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.save, color: Colors.white), - const SizedBox(width: 8), - Text( - _isAtBottom ? 'SAUVEGARDER LA SESSION' : 'Sauvegarder', - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 16 + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 16, + ), + child: FittedBox( + fit: BoxFit.scaleDown, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.save, color: Colors.white), + const SizedBox(width: 8), + Text( + _isAtBottom + ? 'SAUVEGARDER LA SESSION' + : 'Sauvegarder', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ], + ), ), ), - ], - ), + ), ), ), - ), - ), ), ), ), - ), - ], - ); + ], + ); } - Widget _buildZoomableImageWithOverlay(BuildContext context, AnalysisProvider provider) { + Widget _buildZoomableImageWithOverlay( + BuildContext context, + AnalysisProvider provider, + ) { return ClipRect( child: Stack( fit: StackFit.expand, @@ -616,7 +746,9 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { groupingCenterX: provider.groupingResult?.centerX, groupingCenterY: provider.groupingResult?.centerY, groupingDiameter: provider.groupingResult?.diameter, - referenceImpacts: _isSelectingReferences ? provider.referenceImpacts : null, + referenceImpacts: _isSelectingReferences + ? provider.referenceImpacts + : null, ), ], ), @@ -649,7 +781,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ); } - Widget _buildFullscreenEditContent(BuildContext context, AnalysisProvider provider) { + Widget _buildFullscreenEditContent( + BuildContext context, + AnalysisProvider provider, + ) { return Stack( fit: StackFit.expand, children: [ @@ -684,7 +819,8 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ringCount: provider.ringCount, ringRadii: provider.ringRadii, zoomScale: _currentZoomScale, - onShotTapped: (shot) => _showShotOptions(context, provider, shot.id), + onShotTapped: (shot) => + _showShotOptions(context, provider, shot.id), onAddShot: (x, y) { provider.addShot(x, y); }, @@ -714,12 +850,17 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { const Expanded( child: Text( 'Mode édition - Touchez pour ajouter des impacts', - style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), ), ), Text( '${provider.shotCount} impacts', - style: TextStyle(color: Colors.white.withValues(alpha: 0.8)), + style: TextStyle( + color: Colors.white.withValues(alpha: 0.8), + ), ), ], ), @@ -825,7 +966,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { Expanded( child: ElevatedButton.icon( onPressed: provider.referenceImpacts.length >= 2 - ? () => _showCalibratedDetectionDialog(context, provider) + ? () => _showCalibratedDetectionDialog( + context, + provider, + ) : null, icon: const Icon(Icons.auto_fix_high), label: const Text('Detecter'), @@ -876,8 +1020,8 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { // ), const SizedBox(height: 12), ], + // Manual actions - ], ); } @@ -891,7 +1035,9 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('1. Calibrez d\'abord la cible en utilisant le bouton de calibration'), + Text( + '1. Calibrez d\'abord la cible en utilisant le bouton de calibration', + ), SizedBox(height: 8), Text('2. Appuyez sur l\'image pour ajouter un impact manuellement'), SizedBox(height: 8), @@ -910,7 +1056,11 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ); } - void _showShotOptions(BuildContext context, AnalysisProvider provider, String shotId) { + void _showShotOptions( + BuildContext context, + AnalysisProvider provider, + String shotId, + ) { showModalBottomSheet( context: context, builder: (context) => SafeArea( @@ -957,7 +1107,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { } Navigator.pop(context); }, - child: const Text('Effacer', style: TextStyle(color: AppTheme.errorColor)), + child: const Text( + 'Effacer', + style: TextStyle(color: AppTheme.errorColor), + ), ), ], ), @@ -1017,7 +1170,9 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { const SizedBox(height: 12), // Circularity slider - Text('Circularite minimum: ${(minCircularity * 100).toStringAsFixed(0)}%'), + Text( + 'Circularite minimum: ${(minCircularity * 100).toStringAsFixed(0)}%', + ), Slider( value: minCircularity, min: 0.3, @@ -1035,7 +1190,9 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { const SizedBox(height: 12), // Fill ratio slider - Text('Remplissage minimum: ${(minFillRatio * 100).toStringAsFixed(0)}%'), + Text( + 'Remplissage minimum: ${(minFillRatio * 100).toStringAsFixed(0)}%', + ), Slider( value: minFillRatio, min: 0.3, @@ -1110,7 +1267,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { SizedBox( width: 20, height: 20, - child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), ), SizedBox(width: 12), Text('Detection en cours...'), @@ -1140,7 +1300,9 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ? '$count impact(s) detecte(s)' : 'Aucun impact detecte. Essayez d\'ajuster les parametres.', ), - backgroundColor: count > 0 ? AppTheme.successColor : AppTheme.warningColor, + backgroundColor: count > 0 + ? AppTheme.successColor + : AppTheme.warningColor, ), ); } @@ -1154,7 +1316,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ); } - void _showCalibratedDetectionDialog(BuildContext context, AnalysisProvider provider) { + void _showCalibratedDetectionDialog( + BuildContext context, + AnalysisProvider provider, + ) { double tolerance = 2.0; bool clearExisting = true; // NOTE: OpenCV désactivé - problèmes de build Windows @@ -1182,7 +1347,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { padding: const EdgeInsets.all(12), child: Row( children: [ - const Icon(Icons.info_outline, color: Colors.deepPurple), + const Icon( + Icons.info_outline, + color: Colors.deepPurple, + ), const SizedBox(width: 8), Expanded( child: Text( @@ -1254,7 +1422,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { SizedBox( width: 20, height: 20, - child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), ), SizedBox(width: 12), Text('Apprentissage des references...'), @@ -1272,7 +1443,9 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('Impossible d\'analyser les references. Essayez de selectionner d\'autres impacts.'), + content: Text( + 'Impossible d\'analyser les references. Essayez de selectionner d\'autres impacts.', + ), backgroundColor: AppTheme.errorColor, ), ); @@ -1290,7 +1463,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { SizedBox( width: 20, height: 20, - child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), ), SizedBox(width: 12), Text('Detection en cours...'), @@ -1323,7 +1499,9 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ? '$count impact(s) detecte(s) a partir des references' : 'Aucun impact detecte. Essayez d\'augmenter la tolerance.', ), - backgroundColor: count > 0 ? AppTheme.successColor : AppTheme.warningColor, + backgroundColor: count > 0 + ? AppTheme.successColor + : AppTheme.warningColor, ), ); } @@ -1346,7 +1524,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ); } - Future _saveSession(BuildContext context, AnalysisProvider provider) async { + Future _saveSession( + BuildContext context, + AnalysisProvider provider, + ) async { final notesController = TextEditingController(); final shouldSave = await showDialog( @@ -1376,7 +1557,9 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { if (shouldSave == true) { try { - await provider.saveSession(notes: notesController.text.isEmpty ? null : notesController.text); + await provider.saveSession( + notes: notesController.text.isEmpty ? null : notesController.text, + ); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(