feat: boutons valider flottants + reset impacts dans l'éditeur, export IA dans la popup fin de session

- 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 <noreply@anthropic.com>
This commit is contained in:
qlionbleusam
2026-07-22 17:22:37 +02:00
co-authored by Claude Opus 4.8
parent df5f84c87c
commit b641e80d62
2 changed files with 88 additions and 89 deletions
+67 -82
View File
@@ -221,87 +221,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
} }
}, },
), ),
actions: [ actions: const [],
// 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<bool>(
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<AnalysisProvider>();
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<SessionProvider>();
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,
),
),
),
],
), ),
body: Stack( body: Stack(
children: [ children: [
@@ -531,7 +451,17 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
? EdgeInsets.zero ? EdgeInsets.zero
: const EdgeInsets.all(16.0), : const EdgeInsets.all(16.0),
child: _isCalibrating 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( : FloatingActionButton.extended(
onPressed: () => onPressed: () =>
_showSaveSessionDialog(context, provider), _showSaveSessionDialog(context, provider),
@@ -611,6 +541,61 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
Text('Score total: ${provider.totalScore}'), Text('Score total: ${provider.totalScore}'),
const SizedBox(height: 16), const SizedBox(height: 16),
const Text('Voulez-vous enregistrer cette session ?'), 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<bool>(
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<SessionProvider>();
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: [ actions: [
@@ -20,6 +20,7 @@ import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../core/theme/app_theme.dart';
import '../../data/models/shot.dart'; import '../../data/models/shot.dart';
import 'analysis_provider.dart'; import 'analysis_provider.dart';
import 'widgets/target_overlay.dart'; import 'widgets/target_overlay.dart';
@@ -99,16 +100,29 @@ class _ImpactEditorScreenState extends State<ImpactEditorScreen> {
title: Text('Placement des impacts (${provider.shotCount})'), title: Text('Placement des impacts (${provider.shotCount})'),
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
tooltip: 'Retour à la calibration', tooltip: 'Retour au plotting',
onPressed: () => Navigator.pop(context, false), 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), onPressed: () => Navigator.pop(context, true),
child: const Text( backgroundColor: AppTheme.primaryColor,
'VALIDER', icon: const Icon(Icons.check),
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), label: const Text('VALIDER'),
),
), ),
], ],
), ),