diff --git a/lib/features/analysis/analysis_provider.dart b/lib/features/analysis/analysis_provider.dart index 27b9f05c..494c38ee 100644 --- a/lib/features/analysis/analysis_provider.dart +++ b/lib/features/analysis/analysis_provider.dart @@ -496,13 +496,13 @@ class AnalysisProvider extends ChangeNotifier { /// Adjust target position void adjustTargetPosition( - double centerX, - double centerY, - double innerRadius, - double radius, { - int? ringCount, - List? ringRadii, - }) { + double centerX, + double centerY, + double innerRadius, + double radius, { + int? ringCount, + List? ringRadii, + }) { _targetCenterX = centerX; _targetCenterY = centerY; _targetInnerRadius = innerRadius; @@ -510,9 +510,9 @@ class AnalysisProvider extends ChangeNotifier { if (ringCount != null) { _ringCount = ringCount; } - if (ringRadii != null) { - _ringRadii = ringRadii; - } + + // CORRECTION : On accepte désormais la valeur null pour pouvoir réinitialiser l'espacement ! + _ringRadii = ringRadii; // Recalculate all shot scores based on new target position _shots = _shots.map((shot) { diff --git a/lib/features/analysis/widgets/target_calibration.dart b/lib/features/analysis/widgets/target_calibration.dart index 0bb96172..eac26794 100644 --- a/lib/features/analysis/widgets/target_calibration.dart +++ b/lib/features/analysis/widgets/target_calibration.dart @@ -51,14 +51,14 @@ class TargetCalibrationState extends State { late int _ringCount; late List _ringRadii; + // CORRECTION : Variable dédiée pour piloter la jauge orange sans conflit + late double _currentEspacementRatio; + bool _isDraggingCenter = false; bool _isDraggingRadius = false; bool _isDraggingInnerRadius = false; - // Contrôle de l'affichage du scroll d'espacement (désactivé par défaut) bool _showEspacement = false; - - // Variable temporaire pour stocker la valeur du rayon au début du pincement tactile double _baseRadiusBeforeScale = 1.0; @override @@ -69,6 +69,9 @@ class TargetCalibrationState extends State { _radius = widget.initialRadius; _innerRadius = widget.initialInnerRadius; _ringCount = widget.initialRingCount; + + // Initialisation du ratio par défaut + _currentEspacementRatio = (_radius > 0) ? (_innerRadius / _radius).clamp(0.01, 0.9) : 0.1; _initRingRadii(); } @@ -77,11 +80,9 @@ class TargetCalibrationState extends State { widget.initialRingRadii!.length == _ringCount) { _ringRadii = List.from(widget.initialRingRadii!); } else { - // Initialize with default proportional radii interpolated between inner and outer _ringRadii = List.generate(_ringCount, (i) { if (_ringCount <= 1) return 1.0; - final ratio = _innerRadius / _radius; - return ratio + (1.0 - ratio) * i / (_ringCount - 1); + return _currentEspacementRatio + (1.0 - _currentEspacementRatio) * i / (_ringCount - 1); }); } } @@ -91,12 +92,10 @@ class TargetCalibrationState extends State { super.didUpdateWidget(oldWidget); bool shouldReinit = false; - if (widget.initialCenterX != oldWidget.initialCenterX && - !_isDraggingCenter) { + if (widget.initialCenterX != oldWidget.initialCenterX && !_isDraggingCenter) { _centerX = widget.initialCenterX; } - if (widget.initialCenterY != oldWidget.initialCenterY && - !_isDraggingCenter) { + if (widget.initialCenterY != oldWidget.initialCenterY && !_isDraggingCenter) { _centerY = widget.initialCenterY; } if (widget.initialRingCount != oldWidget.initialRingCount) { @@ -107,12 +106,12 @@ class TargetCalibrationState extends State { _radius = widget.initialRadius; shouldReinit = true; } - if (widget.initialInnerRadius != oldWidget.initialInnerRadius && - !_isDraggingInnerRadius) { + if (widget.initialInnerRadius != oldWidget.initialInnerRadius && !_isDraggingInnerRadius) { _innerRadius = widget.initialInnerRadius; + _currentEspacementRatio = (_radius > 0) ? (_innerRadius / _radius).clamp(0.01, 0.9) : 0.1; shouldReinit = true; } - if (widget.initialRingRadii != oldWidget.initialRingRadii) { + if (widget.initialRingRadii != oldWidget.initialRingRadii && widget.initialRingRadii != null) { shouldReinit = true; } @@ -129,7 +128,6 @@ class TargetCalibrationState extends State { return Stack( children: [ - // Utilisation d'un Gesture Detector basé sur le Scale pour synchroniser à 1 ou 2 doigts GestureDetector( onScaleStart: (details) { _baseRadiusBeforeScale = _radius; @@ -162,7 +160,6 @@ class TargetCalibrationState extends State { ), ), - // Panneau de configuration des Sliders en haut Positioned( top: 10, left: 40, @@ -170,7 +167,6 @@ class TargetCalibrationState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - // Ligne d'activation pour afficher ou masquer l'espacement Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2), @@ -201,7 +197,6 @@ class TargetCalibrationState extends State { ), ), - // Slider pour la taille (toujours visible) Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), decoration: BoxDecoration( @@ -221,6 +216,7 @@ class TargetCalibrationState extends State { onChanged: (value) { setState(() { _radius = value; + _innerRadius = _radius * _currentEspacementRatio; _initRingRadii(forceRecalculate: true); }); _notifyChange(); @@ -232,11 +228,10 @@ class TargetCalibrationState extends State { ), ), - // Affichage conditionnel du slider d'espacement orange if (_showEspacement) ...[ const SizedBox(height: 8), Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + padding: const EdgeInsets.fromLTRB(16, 4, 4, 4), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(20), @@ -246,21 +241,51 @@ class TargetCalibrationState extends State { const Text('Espacement ', style: TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold)), const Icon(Icons.compress, color: Colors.white, size: 16), Expanded( - child: Slider( - value: (_innerRadius / _radius).clamp(0.01, 0.9), - min: 0.01, - max: 0.9, - activeColor: Colors.orange, - onChanged: (value) { - setState(() { - _innerRadius = _radius * value; - _initRingRadii(forceRecalculate: true); - }); - _notifyChange(); - }, + child: Row( + children: [ + Expanded( + child: Slider( + value: _currentEspacementRatio, + min: 0.01, + max: 0.9, + activeColor: Colors.orange, + onChanged: (value) { + setState(() { + _currentEspacementRatio = value; + _innerRadius = _radius * _currentEspacementRatio; + _initRingRadii(forceRecalculate: true); + }); + _notifyChange(); + }, + ), + ), + const Icon(Icons.expand, color: Colors.white, size: 16), + ], ), ), - const Icon(Icons.expand, color: Colors.white, size: 16), + // CORRECTION DU BOUTON RESET : Force visuellement le ratio par défaut + // CORRECTION RADICALE : On force le ratio à 0.1 (10% standard) en dur + IconButton( + icon: const Icon(Icons.refresh, color: Colors.white70, size: 20), + onPressed: () { + setState(() { + // 1. On force la jauge orange à se remettre pile à 10% (la valeur d'usine) + _currentEspacementRatio = 0.1; + + // 2. On réaligne l'innerRadius sur la taille actuelle + _innerRadius = _radius * _currentEspacementRatio; + + // 3. On force la reconstruction des cercles parfaits + _initRingRadii(forceRecalculate: true); + }); + + // 4. On balance tout ça au provider pour qu'il s'aligne + _notifyChange(); + }, + tooltip: 'Réinitialiser l\'espacement', + constraints: const BoxConstraints(), + padding: const EdgeInsets.symmetric(horizontal: 8), + ), ], ), ), @@ -268,7 +293,6 @@ class TargetCalibrationState extends State { ], ), ), - ], ); }, @@ -286,28 +310,16 @@ class TargetCalibrationState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - _buildDirectionButton( - Icons.keyboard_arrow_up, - () => _moveCenterByPixels(0, -1, size), - ), + _buildDirectionButton(Icons.keyboard_arrow_up, () => _moveCenterByPixels(0, -1, size)), Row( mainAxisSize: MainAxisSize.min, children: [ - _buildDirectionButton( - Icons.keyboard_arrow_left, - () => _moveCenterByPixels(-1, 0, size), - ), + _buildDirectionButton(Icons.keyboard_arrow_left, () => _moveCenterByPixels(-1, 0, size)), const SizedBox(width: 40), - _buildDirectionButton( - Icons.keyboard_arrow_right, - () => _moveCenterByPixels(1, 0, size), - ), + _buildDirectionButton(Icons.keyboard_arrow_right, () => _moveCenterByPixels(1, 0, size)), ], ), - _buildDirectionButton( - Icons.keyboard_arrow_down, - () => _moveCenterByPixels(0, 1, size), - ), + _buildDirectionButton(Icons.keyboard_arrow_down, () => _moveCenterByPixels(0, 1, size)), ], ), ); @@ -315,10 +327,7 @@ class TargetCalibrationState extends State { Widget _buildDirectionButton(IconData icon, VoidCallback onPressed) { return Container( - decoration: BoxDecoration( - color: Colors.black87, - borderRadius: BorderRadius.circular(8), - ), + decoration: BoxDecoration(color: Colors.black87, borderRadius: BorderRadius.circular(8)), margin: const EdgeInsets.all(2), child: IconButton( icon: Icon(icon, color: Colors.white, size: 28), @@ -348,22 +357,19 @@ class TargetCalibrationState extends State { _notifyChange(); } - // Traitement intelligent des gestes tactiles void _onScaleUpdate(ScaleUpdateDetails details, Size size) { setState(() { if (details.pointerCount == 2) { - // GESTE À DEUX DOIGTS : On calcule le zoom en respectant le range strict (0.3 à 1.2) _radius = (_baseRadiusBeforeScale * details.scale).clamp(0.5, 1.1); + _innerRadius = _radius * _currentEspacementRatio; _initRingRadii(forceRecalculate: true); } else if (_isDraggingCenter) { - // GESTE À UN SEUL DOIGT : Déplacement fluide du centre de la cible final deltaX = details.focalPointDelta.dx / size.width; final deltaY = details.focalPointDelta.dy / size.height; _centerX = _centerX + deltaX; _centerY = _centerY + deltaY; } }); - _notifyChange(); } @@ -420,31 +426,16 @@ class _CalibrationPainter extends CustomPainter { } if (isDraggingCenter) { - final crosshairLinePaint = Paint() - ..color = Colors.red.withValues(alpha: 0.5) - ..strokeWidth = 1; - canvas.drawLine( - Offset(0, centerPx.dy), - Offset(size.width, centerPx.dy), - crosshairLinePaint, - ); - canvas.drawLine( - Offset(centerPx.dx, 0), - Offset(centerPx.dx, size.height), - crosshairLinePaint, - ); + final crosshairLinePaint = Paint()..color = Colors.red.withValues(alpha: 0.5)..strokeWidth = 1; + canvas.drawLine(Offset(0, centerPx.dy), Offset(size.width, centerPx.dy), crosshairLinePaint); + canvas.drawLine(Offset(centerPx.dx, 0), Offset(centerPx.dx, size.height), crosshairLinePaint); } _drawCenterHandle(canvas, centerPx); _drawInstructions(canvas, size); } - void _drawConcentricZones( - Canvas canvas, - Size size, - Offset center, - double baseRadius, - ) { + void _drawConcentricZones(Canvas canvas, Size size, Offset center, double baseRadius) { List zoneColors = []; for (int i = 0; i < ringCount; i++) { final ratio = i / ringCount; @@ -468,9 +459,7 @@ class _CalibrationPainter extends CustomPainter { ..color = Colors.red.withValues(alpha: 0.8); for (int i = ringCount - 1; i >= 0; i--) { - final ringRadius = ringRadii.length > i - ? ringRadii[i] - : (i + 1) / ringCount; + final ringRadius = ringRadii.length > i ? ringRadii[i] : (i + 1) / ringCount; final zoneRadius = baseRadius * ringRadius; zonePaint.color = zoneColors[i]; @@ -481,12 +470,8 @@ class _CalibrationPainter extends CustomPainter { final textPainter = TextPainter(textDirection: TextDirection.ltr); for (int i = 0; i < ringCount; i++) { - final ringRadius = ringRadii.length > i - ? ringRadii[i] - : (i + 1) / ringCount; - final prevRingRadius = i > 0 - ? (ringRadii.length > i - 1 ? ringRadii[i - 1] : i / ringCount) - : 0.0; + final ringRadius = ringRadii.length > i ? ringRadii[i] : (i + 1) / ringCount; + final prevRingRadius = i > 0 ? (ringRadii.length > i - 1 ? ringRadii[i - 1] : i / ringCount) : 0.0; final zoneRadius = baseRadius * (ringRadius + prevRingRadius) / 2; final score = 10 - i; @@ -506,53 +491,28 @@ class _CalibrationPainter extends CustomPainter { final labelY = center.dy - textPainter.height / 2; if (labelY >= 0 && labelY <= size.height) { - textPainter.paint( - canvas, - Offset(labelX - textPainter.width / 2, labelY), - ); + textPainter.paint(canvas, Offset(labelX - textPainter.width / 2, labelY)); } } } - void _drawSilhouetteZones( - Canvas canvas, - Size size, - Offset center, - double radius, - ) { - final paint = Paint() - ..style = PaintingStyle.stroke - ..strokeWidth = 2; - + void _drawSilhouetteZones(Canvas canvas, Size size, Offset center, double radius) { + final paint = Paint()..style = PaintingStyle.stroke..strokeWidth = 2; final silhouetteWidth = radius * 0.8; final silhouetteHeight = radius * 2; paint.color = Colors.green.withValues(alpha: 0.5); - canvas.drawRect( - Rect.fromCenter( - center: center, - width: silhouetteWidth, - height: silhouetteHeight, - ), - paint, - ); + canvas.drawRect(Rect.fromCenter(center: center, width: silhouetteWidth, height: silhouetteHeight), paint); } void _drawCenterHandle(Canvas canvas, Offset center) { - final outerPaint = Paint() - ..color = isDraggingCenter ? Colors.redAccent : Colors.red - ..style = PaintingStyle.stroke - ..strokeWidth = 3; + final outerPaint = Paint()..color = isDraggingCenter ? Colors.redAccent : Colors.red..style = PaintingStyle.stroke..strokeWidth = 3; canvas.drawCircle(center, 15, outerPaint); - final innerPaint = Paint() - ..color = isDraggingCenter ? Colors.redAccent : Colors.red - ..style = PaintingStyle.fill; + final innerPaint = Paint()..color = isDraggingCenter ? Colors.redAccent : Colors.red..style = PaintingStyle.fill; canvas.drawCircle(center, 5, innerPaint); - final crossPaint = Paint() - ..color = isDraggingCenter ? Colors.redAccent : Colors.red - ..strokeWidth = 2; + final crossPaint = Paint()..color = isDraggingCenter ? Colors.redAccent : Colors.red..strokeWidth = 2; canvas.drawLine(Offset(center.dx - 20, center.dy), Offset(center.dx - 8, center.dy), crossPaint); canvas.drawLine(Offset(center.dx + 8, center.dy), Offset(center.dx + 20, center.dy), crossPaint); canvas.drawLine(Offset(center.dx, center.dy - 20), Offset(center.dx, center.dy - 8), crossPaint); @@ -561,23 +521,12 @@ class _CalibrationPainter extends CustomPainter { void _drawInstructions(Canvas canvas, Size size) { const instruction = 'Deplacez le centre ou ajustez le rayon'; - final textPainter = TextPainter( - text: TextSpan( - text: instruction, - style: TextStyle( - color: Colors.white.withValues(alpha: 0.9), - fontSize: 12, - backgroundColor: Colors.black54, - ), - ), + text: TextSpan(text: instruction, style: TextStyle(color: Colors.white.withValues(alpha: 0.9), fontSize: 12, backgroundColor: Colors.black54)), textDirection: TextDirection.ltr, ); textPainter.layout(); - textPainter.paint( - canvas, - Offset((size.width - textPainter.width) / 2, size.height - 30), - ); + textPainter.paint(canvas, Offset((size.width - textPainter.width) / 2, size.height - 30)); } @override diff --git a/lib/features/home/home_screen.dart b/lib/features/home/home_screen.dart index ad242155..76397185 100644 --- a/lib/features/home/home_screen.dart +++ b/lib/features/home/home_screen.dart @@ -35,7 +35,7 @@ class _HomeScreenState extends State { final repository = context.read(); final stats = await repository.getStatistics(); final sessions = await repository.getAllSessions(); - + if (mounted) { setState(() { _stats = stats; @@ -125,21 +125,100 @@ class _HomeScreenState extends State { final sessionProvider = context.watch(); final isSessionActive = sessionProvider.isSessionActive; - return ElevatedButton.icon( - onPressed: () => _navigateToSessionSetup(context), - style: ElevatedButton.styleFrom( - backgroundColor: isSessionActive ? AppTheme.secondaryColor : AppTheme.primaryColor, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 20), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppConstants.borderRadius), + // Si aucune session n'est active, on affiche juste le bouton de départ habituel + if (!isSessionActive) { + return ElevatedButton.icon( + onPressed: () => _navigateToSessionSetup(context), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 20), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + ), ), - ), - icon: Icon(isSessionActive ? Icons.play_circle_outline : Icons.add_circle_outline, size: 28), - label: Text( - isSessionActive ? 'Continuer la session' : 'Démarrer la session', - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), + icon: const Icon(Icons.add_circle_outline, size: 28), + label: const Text( + 'Démarrer la session', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ); + } + + // MODIFICATION : Alignement sur une seule ligne (80% / 20%) avec une croix pour l'annulation + return Row( + children: [ + // 80% de largeur pour continuer la session + Expanded( + flex: 8, + child: ElevatedButton.icon( + onPressed: () => _navigateToSessionSetup(context), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.secondaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 20), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + ), + ), + icon: const Icon(Icons.play_circle_outline, size: 28), + label: const Text( + 'Continuer la session', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + const SizedBox(width: 12), + // 20% de largeur pour terminer/annuler la session (Bouton Croix) + Expanded( + flex: 2, + child: OutlinedButton( + onPressed: () { + // Boite de dialogue de confirmation pour éviter les erreurs + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Terminer la session ?'), + content: const Text('Êtes-vous sûr de vouloir clôturer la session en cours ?'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('ANNULER'), + ), + TextButton( + onPressed: () { + Navigator.pop(context); + // Clôture propre de la session dans le State global + context.read().endSession(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Session terminée avec succès !'), + backgroundColor: AppTheme.successColor, + ), + ); + }, + child: const Text('TERMINER', style: TextStyle(color: Colors.redAccent)), + ), + ], + ); + }, + ); + }, + style: OutlinedButton.styleFrom( + foregroundColor: Colors.redAccent, + side: const BorderSide(color: Colors.redAccent, width: 1.5), + padding: const EdgeInsets.symmetric(vertical: 20), // Même hauteur que le bouton continuer + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppConstants.borderRadius), + ), + ), + child: const Icon(Icons.close, size: 28), + ), + ), + ], ); } @@ -197,17 +276,17 @@ class _HomeScreenState extends State { ), child: _recentSessions.length < 2 ? const Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.show_chart, size: 40, color: Colors.grey), - Text( - 'Pas assez de données', - style: TextStyle(color: Colors.grey, fontSize: 12), - ), - ], - ), - ) + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.show_chart, size: 40, color: Colors.grey), + Text( + 'Pas assez de données', + style: TextStyle(color: Colors.grey, fontSize: 12), + ), + ], + ), + ) : _buildHomeTrendChart(), ), ), @@ -276,7 +355,7 @@ class _HomeScreenState extends State { final sorted = List.from(_recentSessions) ..sort((a, b) => a.createdAt.compareTo(b.createdAt)); final display = - sorted.length > 7 ? sorted.sublist(sorted.length - 7) : sorted; + sorted.length > 7 ? sorted.sublist(sorted.length - 7) : sorted; return LineChart( LineChartData( @@ -372,4 +451,4 @@ class _HomeScreenState extends State { ); _loadStats(); } -} +} \ No newline at end of file