From 3a9e393a205bf12703cc3c10ecd3c37d6544fe09 Mon Sep 17 00:00:00 2001 From: qlionbleusam Date: Tue, 4 Aug 2026 16:25:44 +0200 Subject: [PATCH] feat: score de session en plotting + guidage pour placer le premier impact --- lib/features/analysis/analysis_screen.dart | 101 ++++++++++++++---- lib/features/analysis/widgets/score_card.dart | 48 ++++++++- 2 files changed, 127 insertions(+), 22 deletions(-) diff --git a/lib/features/analysis/analysis_screen.dart b/lib/features/analysis/analysis_screen.dart index 13ced5b2..a3aa4be3 100644 --- a/lib/features/analysis/analysis_screen.dart +++ b/lib/features/analysis/analysis_screen.dart @@ -297,6 +297,74 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ); } + /// Indication affichée sous le titre en mode Plotting. + /// + /// Rien n'indiquait comment ajouter un impact une fois la calibration + /// validée : ce rappel pointe vers le geste (toucher la cible). + Widget _buildPlottingHint(AnalysisProvider provider) { + final hasShots = provider.shotCount > 0; + + return Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + hasShots ? Icons.touch_app : Icons.add_location_alt, + size: 16, + color: AppTheme.primaryColor, + ), + const SizedBox(width: 6), + Flexible( + child: Text( + hasShots + ? 'Touchez la cible pour modifier vos impacts' + : 'Touchez la cible pour placer vos impacts', + style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500), + ), + ), + ], + ), + ); + } + + /// Bouton flottant du bas de l'écran. + /// + /// En calibration : VALIDER. En plotting : tant qu'aucun impact n'est placé, + /// on ne propose pas de terminer la session mais de placer un impact (le + /// bouton ouvre l'éditeur, comme un tap sur la cible). + Widget _buildBottomAction(BuildContext context, AnalysisProvider provider) { + if (_isCalibrating) { + return FloatingActionButton.extended( + // Même bouton bleu flottant que « TERMINER LA SESSION » : + // on fige la calibration puis on bascule sur le Plotting. + onPressed: () { + _calibrationKey.currentState?.commitCalibration(); + setState(() => _isCalibrating = false); + }, + backgroundColor: AppTheme.primaryColor, + icon: const Icon(Icons.check), + label: const Text('VALIDER'), + ); + } + + if (provider.shotCount == 0) { + return FloatingActionButton.extended( + onPressed: () => _openImpactEditor(provider), + backgroundColor: AppTheme.primaryColor, + icon: const Icon(Icons.add_location_alt), + label: const Text('PLACER UN IMPACT'), + ); + } + + return FloatingActionButton.extended( + onPressed: () => _showSaveSessionDialog(context, provider), + backgroundColor: AppTheme.primaryColor, + icon: const Icon(Icons.save), + label: const Text('TERMINER LA SESSION'), + ); + } + @override Widget build(BuildContext context) { final provider = context.watch(); @@ -355,7 +423,10 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { // Réglages de calibration : au-dessus de la photo pour ne rien // masquer de la cible. - if (_isCalibrating) _buildCalibrationSettings(provider), + if (_isCalibrating) + _buildCalibrationSettings(provider) + else + _buildPlottingHint(provider), AspectRatio( aspectRatio: provider.imageAspectRatio, @@ -450,6 +521,14 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { shotCount: provider.shotCount, scoreResult: provider.scoreResult, targetType: provider.targetType!, + // Cumul de la session : cibles déjà validées + cible + // en cours (absent hors session). + sessionTotalScore: sessionProvider.isSessionActive + ? sessionProvider.totalSessionScore + + provider.totalScore + : null, + sessionTargetCount: + sessionProvider.targetCount + 1, ), const SizedBox(height: 12), if (provider.groupingResult != null && @@ -562,25 +641,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { padding: _isAtBottom ? EdgeInsets.zero : const EdgeInsets.all(16.0), - child: _isCalibrating - ? FloatingActionButton.extended( - // Même bouton bleu flottant que « TERMINER LA SESSION » : - // on fige la calibration puis on bascule sur le Plotting. - onPressed: () { - _calibrationKey.currentState?.commitCalibration(); - setState(() => _isCalibrating = false); - }, - backgroundColor: AppTheme.primaryColor, - icon: const Icon(Icons.check), - label: const Text('VALIDER'), - ) - : FloatingActionButton.extended( - onPressed: () => - _showSaveSessionDialog(context, provider), - backgroundColor: AppTheme.primaryColor, - icon: const Icon(Icons.save), - label: const Text('TERMINER LA SESSION'), - ), + child: _buildBottomAction(context, provider), ), ), ), diff --git a/lib/features/analysis/widgets/score_card.dart b/lib/features/analysis/widgets/score_card.dart index 2b45d70d..8007618a 100644 --- a/lib/features/analysis/widgets/score_card.dart +++ b/lib/features/analysis/widgets/score_card.dart @@ -17,12 +17,22 @@ class ScoreCard extends StatelessWidget { final ScoreResult? scoreResult; final TargetType targetType; + /// Score cumulé de la session (cibles déjà validées + cible en cours). + /// + /// null hors session : le bandeau de session n'est alors pas affiché. + final int? sessionTotalScore; + + /// Nombre de cibles comptabilisées dans [sessionTotalScore]. + final int sessionTargetCount; + const ScoreCard({ super.key, required this.totalScore, required this.shotCount, this.scoreResult, required this.targetType, + this.sessionTotalScore, + this.sessionTargetCount = 0, }); @override @@ -46,13 +56,23 @@ class ScoreCard extends StatelessWidget { ), ), const Spacer(), + if (sessionTotalScore != null) ...[ + Flexible(child: _buildSessionBadge(context)), + const SizedBox(width: 4), + ], MetricInfoButton( title: 'Score', explanations: [ MetricExplanation( 'Total', - 'Somme des points de tous vos impacts, sur le maximum ' - 'possible (nombre d\'impacts × $maxScore points).', + 'Somme des points de tous vos impacts sur CETTE cible, ' + 'sur le maximum possible (nombre d\'impacts × ' + '$maxScore points).', + ), + const MetricExplanation( + 'Session', + 'Score cumulé de toutes les cibles de la session en ' + 'cours, cible affichée comprise.', ), const MetricExplanation( 'Impacts', @@ -122,6 +142,30 @@ class ScoreCard extends StatelessWidget { ); } + /// Bandeau compact rappelant le score total de la session en cours. + Widget _buildSessionBadge(BuildContext context) { + // Le nombre de cibles n'est rappelé qu'à partir de la deuxième : sur la + // première il n'apporte rien et allonge le bandeau pour rien. + final cibles = sessionTargetCount > 1 ? ' ($sessionTargetCount cibles)' : ''; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: AppTheme.primaryColor.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + 'Session : $sessionTotalScore pts$cibles', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontWeight: FontWeight.bold, + color: AppTheme.primaryColor, + ), + ), + ); + } + Widget _buildScoreStat( BuildContext context, String label,