From b641e80d6222978a10a6ee59275f43dffa504ea7 Mon Sep 17 00:00:00 2001 From: qlionbleusam Date: Wed, 22 Jul 2026 17:22:37 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20boutons=20valider=20flottants=20+=20res?= =?UTF-8?q?et=20impacts=20dans=20l'=C3=A9diteur,=20export=20IA=20dans=20la?= =?UTF-8?q?=20popup=20fin=20de=20session?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - VALIDER en bouton bleu flottant sur calibration et placement des impacts - corbeille (reset impacts) à côté du VALIDER dans l'éditeur - retrait des boutons refresh/cloud de l'AppBar du plotting - export IA déplacé dans la popup de fin de session Co-Authored-By: Claude Opus 4.8 --- lib/features/analysis/analysis_screen.dart | 149 ++++++++---------- .../analysis/impact_editor_screen.dart | 28 +++- 2 files changed, 88 insertions(+), 89 deletions(-) diff --git a/lib/features/analysis/analysis_screen.dart b/lib/features/analysis/analysis_screen.dart index e550f783..5db4a735 100644 --- a/lib/features/analysis/analysis_screen.dart +++ b/lib/features/analysis/analysis_screen.dart @@ -221,87 +221,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { } }, ), - actions: [ - // Remise à zéro des impacts : efface tous les impacts en un clic, - // sans modifier la calibration (centre, rayon, anneaux). - if (!_isCalibrating) - IconButton( - icon: const Icon(Icons.refresh), - tooltip: 'Effacer tous les impacts', - onPressed: () => provider.clearShots(), - ), - // Nuage d'export vers le backend IA : visible uniquement si l'analyse - // a réussi ET que l'utilisateur a activé l'option dans les Paramètres. - if (!_isCalibrating) - FutureBuilder( - future: WalletIdentityService().isUploadEnabled(), - builder: (context, snapshot) { - final isEnabled = snapshot.data ?? false; - if (!isEnabled || - provider.state != AnalysisState.success) { - return const SizedBox.shrink(); - } - return IconButton( - icon: const Icon(Icons.cloud_upload), - tooltip: 'Exporter pour IA', - onPressed: () async { - final p = context.read(); - if (p.state != AnalysisState.success) return; - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Exportation en cours...')), - ); - - // Métadonnées réelles de la session en cours (distance, - // arme) plutôt que les placeholders par défaut. - final sp = context.read(); - final success = await p.exportToAiBackend( - sessionId: sp.activeSessionId, - distance: sp.distance, - weapon: sp.currentWeapon, - ); - - if (!context.mounted) return; - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - if (success) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Export réussi vers le backend IA !'), - backgroundColor: AppTheme.successColor, - ), - ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: - Text(p.errorMessage ?? 'Erreur d\'export'), - backgroundColor: AppTheme.errorColor, - ), - ); - } - }, - ); - }, - ), - if (_isCalibrating) - TextButton( - onPressed: () { - // On fige la calibration courante puis on bascule DIRECTEMENT - // sur le Plotting (lecture seule). Le placement des impacts ne - // s'ouvre plus ici : il se fera depuis le Plotting en tapant la - // cible. - _calibrationKey.currentState?.commitCalibration(); - setState(() => _isCalibrating = false); - }, - child: const Text( - 'VALIDER', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - ), - ], + actions: const [], ), body: Stack( children: [ @@ -531,7 +451,17 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ? EdgeInsets.zero : const EdgeInsets.all(16.0), child: _isCalibrating - ? const SizedBox.shrink() + ? 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), @@ -611,6 +541,61 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { Text('Score total: ${provider.totalScore}'), const SizedBox(height: 16), const Text('Voulez-vous enregistrer cette session ?'), + // Export vers le backend IA : proposé ici (et non plus dans + // l'AppBar). Visible seulement si l'option est activée dans les + // Paramètres ET si l'analyse a réussi. + FutureBuilder( + future: WalletIdentityService().isUploadEnabled(), + builder: (context, snapshot) { + final isEnabled = snapshot.data ?? false; + if (!isEnabled || + provider.state != AnalysisState.success) { + return const SizedBox.shrink(); + } + return Padding( + padding: const EdgeInsets.only(top: 12), + child: OutlinedButton.icon( + icon: const Icon(Icons.cloud_upload), + label: const Text('Exporter pour IA'), + onPressed: () async { + if (provider.state != AnalysisState.success) return; + + // On capture messenger et session AVANT l'await pour + // éviter tout usage de context après un gap async. + final messenger = ScaffoldMessenger.of(context); + final sp = context.read(); + + messenger.showSnackBar( + const SnackBar( + content: Text('Exportation en cours...'), + ), + ); + + final success = await provider.exportToAiBackend( + sessionId: sp.activeSessionId, + distance: sp.distance, + weapon: sp.currentWeapon, + ); + + messenger.hideCurrentSnackBar(); + messenger.showSnackBar( + SnackBar( + content: Text( + success + ? 'Export réussi vers le backend IA !' + : (provider.errorMessage ?? + 'Erreur d\'export'), + ), + backgroundColor: success + ? AppTheme.successColor + : AppTheme.errorColor, + ), + ); + }, + ), + ); + }, + ), ], ), actions: [ diff --git a/lib/features/analysis/impact_editor_screen.dart b/lib/features/analysis/impact_editor_screen.dart index 9d395651..ea180928 100644 --- a/lib/features/analysis/impact_editor_screen.dart +++ b/lib/features/analysis/impact_editor_screen.dart @@ -20,6 +20,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import '../../core/theme/app_theme.dart'; import '../../data/models/shot.dart'; import 'analysis_provider.dart'; import 'widgets/target_overlay.dart'; @@ -99,16 +100,29 @@ class _ImpactEditorScreenState extends State { title: Text('Placement des impacts (${provider.shotCount})'), leading: IconButton( icon: const Icon(Icons.arrow_back), - tooltip: 'Retour à la calibration', + tooltip: 'Retour au plotting', onPressed: () => Navigator.pop(context, false), ), - actions: [ - TextButton( + ), + // Corbeille (efface tous les impacts) + bouton bleu flottant VALIDER. + // heroTag distinct sur chaque FAB pour éviter le conflit de Hero. + floatingActionButton: Row( + mainAxisSize: MainAxisSize.min, + children: [ + FloatingActionButton( + heroTag: 'reset_impacts', + onPressed: () => provider.clearShots(), + backgroundColor: Colors.grey.shade800, + tooltip: 'Effacer tous les impacts', + child: const Icon(Icons.delete), + ), + const SizedBox(width: 12), + FloatingActionButton.extended( + heroTag: 'validate_impacts', onPressed: () => Navigator.pop(context, true), - child: const Text( - 'VALIDER', - style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), - ), + backgroundColor: AppTheme.primaryColor, + icon: const Icon(Icons.check), + label: const Text('VALIDER'), ), ], ),