diff --git a/lib/features/analysis/analysis_provider.dart b/lib/features/analysis/analysis_provider.dart index e2e1a6d9..92270c79 100644 --- a/lib/features/analysis/analysis_provider.dart +++ b/lib/features/analysis/analysis_provider.dart @@ -162,8 +162,10 @@ class AnalysisProvider extends ChangeNotifier { return; } - // CORRECTION PARALLÈLE : Changement de la méthode pour l'Isolate asynchrone - final result = await _detectionService.detectTargetAsync(imagePath, targetType); + final result = await _detectionService.detectTargetAsync( + imagePath, + targetType, + ); if (!result.success) { _state = AnalysisState.error; @@ -512,11 +514,14 @@ class AnalysisProvider extends ChangeNotifier { double radius, { int? ringCount, List? ringRadii, + double zoomScale = 1.0, + Offset offset = Offset.zero, }) { - _targetCenterX = centerX; - _targetCenterY = centerY; - _targetInnerRadius = innerRadius; - _targetRadius = radius; + _targetCenterX = (centerX - offset.dx) / zoomScale; + _targetCenterY = (centerY - offset.dy) / zoomScale; + _targetRadius = radius / zoomScale; + _targetInnerRadius = innerRadius / zoomScale; + if (ringCount != null) { _ringCount = ringCount; } @@ -720,14 +725,14 @@ class AnalysisProvider extends ChangeNotifier { return success; } - /// Save the session (Refactorisé pour appliquer le paramètre de date personnalisé) + /// Save the session Future saveSession({ String? notes, String? sessionId, String? weaponName, String? weaponId, int? distance, - DateTime? date, // <-- REÇOIT PROPREMENT LA DATE DE DEBUT DE SESSION + DateTime? date, }) async { if (_imagePath == null || _targetType == null) { throw Exception('Cannot save: missing image or target type'); @@ -751,7 +756,10 @@ class AnalysisProvider extends ChangeNotifier { if (sessionId != null && sessionId != 'standalone') { final existingSession = await _sessionRepository.getSession(sessionId); if (existingSession != null) { - await _sessionRepository.addAnalysisToSession(sessionId, targetAnalysis); + await _sessionRepository.addAnalysisToSession( + sessionId, + targetAnalysis, + ); } else { // CORRECTION : Utilise la date injectée plutôt que DateTime.now() await _sessionRepository.createSession( @@ -788,7 +796,7 @@ class AnalysisProvider extends ChangeNotifier { _errorMessage = null; _imagePath = null; _targetType = null; - _cropRotation = 0.0; // Reset de la rotation + _cropRotation = 0.0; _targetCenterX = 0.5; _targetCenterY = 0.5; _targetRadius = 0.4; diff --git a/lib/features/analysis/analysis_screen.dart b/lib/features/analysis/analysis_screen.dart index 274bac98..70f4b6fd 100644 --- a/lib/features/analysis/analysis_screen.dart +++ b/lib/features/analysis/analysis_screen.dart @@ -49,7 +49,8 @@ class AnalysisScreen extends StatelessWidget { @override Widget build(BuildContext context) { // Reconstitution de l'Offset pour le traitement métier en arrière-plan - final manualCenterOffset = (initialCenterX != null && initialCenterY != null) + final manualCenterOffset = + (initialCenterX != null && initialCenterY != null) ? Offset(initialCenterX!, initialCenterY!) : null; @@ -65,7 +66,12 @@ class AnalysisScreen extends StatelessWidget { if (cropRotation != null) { p.setCropRotation(cropRotation!); } - p.analyzeImage(imagePath, targetType, autoAnalyze: false, manualCenter: manualCenterOffset); + p.analyzeImage( + imagePath, + targetType, + autoAnalyze: false, + manualCenter: manualCenterOffset, + ); return p; }, child: _AnalysisScreenContent( @@ -93,7 +99,8 @@ class _AnalysisScreenContent extends StatefulWidget { } class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { - final GlobalKey _calibrationKey = GlobalKey(); + final GlobalKey _calibrationKey = + GlobalKey(); // Forcé à TRUE pour démarrer sur l'ajustement des cercles bool _isCalibrating = true; @@ -101,7 +108,8 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { bool _isAtBottom = false; final ScrollController _scrollController = ScrollController(); - final TransformationController _transformationController = TransformationController(); + final TransformationController _transformationController = + TransformationController(); final GlobalKey _imageKey = GlobalKey(); double _currentZoomScale = 1.0; String? _movingShotId; @@ -115,7 +123,9 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { void _onScroll() { if (!_scrollController.hasClients) return; - final isBottom = _scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 20; + final isBottom = + _scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 20; if (isBottom != _isAtBottom) { setState(() { _isAtBottom = isBottom; @@ -145,10 +155,14 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { Widget build(BuildContext context) { final provider = context.watch(); final sessionProvider = context.watch(); - final targetNumber = sessionProvider.isSessionActive ? sessionProvider.targetCount + 1 : null; + final targetNumber = sessionProvider.isSessionActive + ? sessionProvider.targetCount + 1 + : null; final titlePrefix = _isCalibrating ? 'Calibration' : 'Plotting'; - final title = targetNumber != null ? '$titlePrefix - Cible $targetNumber' : (_isCalibrating ? 'Calibration' : 'Plotting du Tir'); + final title = targetNumber != null + ? '$titlePrefix - Cible $targetNumber' + : (_isCalibrating ? 'Calibration' : 'Plotting du Tir'); return Scaffold( appBar: AppBar( @@ -178,14 +192,20 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { if (!_isCalibrating && !_isSelectingReferences) IconButton( icon: const Icon(Icons.refresh), - onPressed: () => provider.analyzeImage(context.read().imagePath!, context.read().targetType!), + onPressed: () => provider.analyzeImage( + context.read().imagePath!, + context.read().targetType!, + ), ), if (_isCalibrating) TextButton( onPressed: () => setState(() => _isCalibrating = false), child: const Text( 'TERMINER', - style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), ), ), ], @@ -212,7 +232,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ), ), - // SECTION CORRIGÉE : Utilise maintenant la réactivité du provider globale AspectRatio( aspectRatio: provider.imageAspectRatio, child: _isCalibrating @@ -221,19 +240,26 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { children: [ ClipRect( child: Builder( - builder: (context) { - return Transform( - transform: Matrix4.identity() - ..setTranslationRaw(widget.cropOffset?.dx ?? 0.0, widget.cropOffset?.dy ?? 0.0, 0.0) - ..scale(1.0, 1.0) - ..rotateZ((provider.cropRotation) * (math.pi / 180)), - alignment: Alignment.center, - child: Image.file( - File(provider.imagePath!), - fit: BoxFit.contain, + builder: (context) { + return Transform( + transform: Matrix4.identity() + ..setTranslationRaw( + widget.cropOffset?.dx ?? 0.0, + widget.cropOffset?.dy ?? 0.0, + 0.0, + ) + ..scale(1.0, 1.0) + ..rotateZ( + (provider.cropRotation) * + (math.pi / 180), ), - ); - } + alignment: Alignment.center, + child: Image.file( + File(provider.imagePath!), + fit: BoxFit.contain, + ), + ); + }, ), ), TargetCalibration( @@ -245,7 +271,15 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { initialRingCount: provider.ringCount, initialRingRadii: provider.ringRadii, targetType: provider.targetType!, - onCalibrationChanged: (centerX, centerY, innerRadius, radius, ringCount, {ringRadii}) { + onCalibrationChanged: + ( + centerX, + centerY, + innerRadius, + radius, + ringCount, { + ringRadii, + }) { provider.adjustTargetPosition( centerX, centerY, @@ -253,6 +287,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { radius, ringCount: ringCount, ringRadii: ringRadii, + zoomScale: widget.cropScale ?? 1.0, ); }, ), @@ -269,10 +304,18 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { Card( color: AppTheme.primaryColor.withValues(alpha: 0.1), child: ListTile( - leading: const Icon(Icons.tune, color: AppTheme.primaryColor), + leading: const Icon( + Icons.tune, + color: AppTheme.primaryColor, + ), title: const Text('Ajuster la calibration'), - subtitle: const Text('Modifier le centre ou le rayon global'), - trailing: const Icon(Icons.arrow_forward_ios, size: 16), + subtitle: const Text( + 'Modifier le centre ou le rayon global', + ), + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + ), onTap: () => setState(() => _isCalibrating = true), ), ), @@ -284,7 +327,8 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { targetType: provider.targetType!, ), const SizedBox(height: 12), - if (provider.groupingResult != null && provider.shotCount > 1) + if (provider.groupingResult != null && + provider.shotCount > 1) GroupingStats( groupingResult: provider.groupingResult!, targetCenterX: provider.targetCenterX, @@ -303,16 +347,24 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { children: [ const Text( 'Ajustement precis (pixel par pixel)', - style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Colors.white70), + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Colors.white70, + ), ), 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), @@ -324,19 +376,33 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { children: [ const Text( 'Instructions de calibration', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), ), const SizedBox(height: 12), - _buildInstructionItem(Icons.open_with, 'Glissez le centre pour positionner le centre de la cible'), - _buildInstructionItem(Icons.zoom_out_map, 'Pincez l\'ecran a 2 doigts ou utilisez la jauge pour la taille'), - _buildInstructionItem(Icons.visibility, 'Appuyez sur TERMINER en haut a droite pour valider'), + _buildInstructionItem( + Icons.open_with, + 'Glissez le centre pour positionner le centre de la cible', + ), + _buildInstructionItem( + Icons.zoom_out_map, + 'Pincez l\'ecran a 2 doigts ou utilisez la jauge pour la taille', + ), + _buildInstructionItem( + Icons.visibility, + 'Appuyez sur TERMINER en haut a droite pour valider', + ), const SizedBox(height: 16), Row( children: [ 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, + ), ), ], ), @@ -345,7 +411,9 @@ 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, + ), ), ], ), @@ -364,13 +432,18 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { left: 0, right: 0, child: Align( - alignment: _isAtBottom ? Alignment.bottomCenter : Alignment.bottomRight, + alignment: _isAtBottom + ? Alignment.bottomCenter + : Alignment.bottomRight, child: Padding( - padding: _isAtBottom ? EdgeInsets.zero : const EdgeInsets.all(16.0), + padding: _isAtBottom + ? EdgeInsets.zero + : const EdgeInsets.all(16.0), child: _isCalibrating ? const SizedBox.shrink() : FloatingActionButton.extended( - onPressed: () => _showSaveSessionDialog(context, provider), + onPressed: () => + _showSaveSessionDialog(context, provider), backgroundColor: AppTheme.primaryColor, icon: const Icon(Icons.save), label: const Text('TERMINER LA SESSION'), @@ -396,7 +469,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ); } - Widget _buildZoomableImageWithOverlay(BuildContext context, AnalysisProvider provider) { + Widget _buildZoomableImageWithOverlay( + BuildContext context, + AnalysisProvider provider, + ) { return InteractiveViewer( transformationController: _transformationController, minScale: 1.0, @@ -404,15 +480,19 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { boundaryMargin: const EdgeInsets.all(double.infinity), panEnabled: _movingShotId == null, child: Transform( - // CORRECTION GÉOMÉTRIQUE : On déplace le Transform ICI pour englober le GestureDetector et le Stack d'un coup ! transform: Matrix4.identity() - ..setTranslationRaw(widget.cropOffset?.dx ?? 0.0, widget.cropOffset?.dy ?? 0.0, 0.0) + ..setTranslationRaw( + widget.cropOffset?.dx ?? 0.0, + widget.cropOffset?.dy ?? 0.0, + 0.0, + ) ..scale(1.0, 1.0) ..rotateZ((provider.cropRotation) * (math.pi / 180)), alignment: Alignment.center, child: GestureDetector( onDoubleTapDown: (TapDownDetails details) { - final RenderBox? box = _imageKey.currentContext?.findRenderObject() as RenderBox?; + final RenderBox? box = + _imageKey.currentContext?.findRenderObject() as RenderBox?; if (box == null) return; final localOffset = box.globalToLocal(details.globalPosition); @@ -441,7 +521,8 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { } }, onLongPressStart: (LongPressStartDetails details) { - final RenderBox? box = _imageKey.currentContext?.findRenderObject() as RenderBox?; + final RenderBox? box = + _imageKey.currentContext?.findRenderObject() as RenderBox?; if (box == null) return; final localOffset = box.globalToLocal(details.globalPosition); @@ -474,10 +555,12 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { onLongPressMoveUpdate: (LongPressMoveUpdateDetails details) { if (_movingShotId == null) return; - final RenderBox? box = _imageKey.currentContext?.findRenderObject() as RenderBox?; + final RenderBox? box = + _imageKey.currentContext?.findRenderObject() as RenderBox?; if (box == null) return; - final adjustedGlobalPosition = details.globalPosition + const Offset(-25, -35); + final adjustedGlobalPosition = + details.globalPosition + const Offset(-25, -35); final localOffset = box.globalToLocal(adjustedGlobalPosition); final relX = (localOffset.dx / box.size.width).clamp(0.0, 1.0); @@ -507,7 +590,8 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { shots: provider.shots, showRings: true, zoomScale: _currentZoomScale, - onShotTapped: (shot) => _showShotDetails(context, provider, shot), + onShotTapped: (shot) => + _showShotDetails(context, provider, shot), onAddShot: (relX, relY) => provider.addShot(relX, relY), ), ], @@ -516,11 +600,16 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ), ); } + Widget _buildActionButtons(BuildContext context, AnalysisProvider provider) { return const Column(children: [Row(children: [])]); } - void _showShotDetails(BuildContext context, AnalysisProvider provider, Shot shot) { + void _showShotDetails( + BuildContext context, + AnalysisProvider provider, + Shot shot, + ) { showModalBottomSheet( context: context, builder: (context) => Container( @@ -534,7 +623,9 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ), Text( 'ID: ${shot.id}', - style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey, fontSize: 10), + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(color: Colors.grey, fontSize: 10), ), const SizedBox(height: 16), ListTile( @@ -543,10 +634,18 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { trailing: DropdownButton( 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)), - )) + .map( + (s) => DropdownMenuItem( + value: s, + child: Text( + '$s', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + ), + ) .toList(), onChanged: (newScore) { if (newScore != null) { @@ -566,7 +665,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { Navigator.pop(context); }, icon: const Icon(Icons.delete, color: Colors.red), - label: const Text('SUPPRIMER', style: TextStyle(color: Colors.red)), + label: const Text( + 'SUPPRIMER', + style: TextStyle(color: Colors.red), + ), ), ), ], @@ -631,14 +733,19 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { Navigator.pop(context); Navigator.pushAndRemoveUntil( context, - MaterialPageRoute(builder: (context) => const CaptureScreen()), + MaterialPageRoute( + builder: (context) => const CaptureScreen(), + ), (route) => route.isFirst, ); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Erreur: $e'), backgroundColor: AppTheme.errorColor), + SnackBar( + content: Text('Erreur: $e'), + backgroundColor: AppTheme.errorColor, + ), ); } } @@ -665,7 +772,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Erreur: $e'), backgroundColor: AppTheme.errorColor), + SnackBar( + content: Text('Erreur: $e'), + backgroundColor: AppTheme.errorColor, + ), ); } }