Feat: Bouton de clôture de session (80/20) sur l'accueil et reset de l'espacement

This commit is contained in:
qguillaume
2026-05-26 14:59:25 +02:00
parent fbd631c8e4
commit 0af6a0d8e2
3 changed files with 196 additions and 168 deletions

View File

@@ -496,13 +496,13 @@ class AnalysisProvider extends ChangeNotifier {
/// Adjust target position
void adjustTargetPosition(
double centerX,
double centerY,
double innerRadius,
double radius, {
int? ringCount,
List<double>? ringRadii,
}) {
double centerX,
double centerY,
double innerRadius,
double radius, {
int? ringCount,
List<double>? 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) {

View File

@@ -51,14 +51,14 @@ class TargetCalibrationState extends State<TargetCalibration> {
late int _ringCount;
late List<double> _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<TargetCalibration> {
_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<TargetCalibration> {
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<TargetCalibration> {
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<TargetCalibration> {
_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<TargetCalibration> {
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<TargetCalibration> {
),
),
// Panneau de configuration des Sliders en haut
Positioned(
top: 10,
left: 40,
@@ -170,7 +167,6 @@ class TargetCalibrationState extends State<TargetCalibration> {
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<TargetCalibration> {
),
),
// 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<TargetCalibration> {
onChanged: (value) {
setState(() {
_radius = value;
_innerRadius = _radius * _currentEspacementRatio;
_initRingRadii(forceRecalculate: true);
});
_notifyChange();
@@ -232,11 +228,10 @@ class TargetCalibrationState extends State<TargetCalibration> {
),
),
// 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<TargetCalibration> {
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<TargetCalibration> {
],
),
),
],
);
},
@@ -286,28 +310,16 @@ class TargetCalibrationState extends State<TargetCalibration> {
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<TargetCalibration> {
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<TargetCalibration> {
_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<Color> 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

View File

@@ -125,21 +125,100 @@ class _HomeScreenState extends State<HomeScreen> {
final sessionProvider = context.watch<SessionProvider>();
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<SessionProvider>().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<HomeScreen> {
),
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<HomeScreen> {
final sorted = List<Session>.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(