/// Écran principal d'analyse - Interface centrale de traitement des cibles. /// /// Affiche la cible avec overlay des anneaux et impacts détectés. /// Permet la calibration, l'ajout manuel d'impacts, la détection automatique, /// et le calcul des scores et statistiques de groupement. library; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../core/constants/app_constants.dart'; import '../../core/theme/app_theme.dart'; import '../../data/models/target_type.dart'; import '../../data/repositories/session_repository.dart'; import '../../services/target_detection_service.dart'; import '../../services/score_calculator_service.dart'; import '../../services/grouping_analyzer_service.dart'; import 'analysis_provider.dart'; import 'widgets/target_overlay.dart'; import 'widgets/target_calibration.dart'; import 'widgets/score_card.dart'; import 'widgets/grouping_stats.dart'; class AnalysisScreen extends StatelessWidget { final String imagePath; final TargetType targetType; const AnalysisScreen({ super.key, required this.imagePath, required this.targetType, }); @override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (context) => AnalysisProvider( detectionService: context.read(), scoreCalculatorService: context.read(), groupingAnalyzerService: context.read(), sessionRepository: context.read(), )..analyzeImage(imagePath, targetType), child: const _AnalysisScreenContent(), ); } } class _AnalysisScreenContent extends StatefulWidget { const _AnalysisScreenContent(); @override State<_AnalysisScreenContent> createState() => _AnalysisScreenContentState(); } class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { bool _isCalibrating = false; bool _isSelectingReferences = false; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(_isCalibrating ? 'Calibration' : 'Analyse'), actions: [ Consumer( builder: (context, provider, _) { if (provider.state != AnalysisState.success) return const SizedBox.shrink(); return IconButton( icon: Icon(_isCalibrating ? Icons.check : Icons.tune), onPressed: () { setState(() => _isCalibrating = !_isCalibrating); }, tooltip: _isCalibrating ? 'Terminer calibration' : 'Calibrer la cible', color: _isCalibrating ? AppTheme.successColor : null, ); }, ), IconButton( icon: const Icon(Icons.help_outline), onPressed: () => _showHelpDialog(context), tooltip: 'Aide', ), ], ), body: Consumer( builder: (context, provider, _) { switch (provider.state) { case AnalysisState.initial: case AnalysisState.loading: return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(), SizedBox(height: 16), Text('Analyse en cours...'), ], ), ); case AnalysisState.error: return Center( child: Padding( padding: const EdgeInsets.all(AppConstants.defaultPadding), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 64, color: AppTheme.errorColor), const SizedBox(height: 16), Text( provider.errorMessage ?? 'Une erreur est survenue', textAlign: TextAlign.center, ), const SizedBox(height: 16), ElevatedButton( onPressed: () => Navigator.pop(context), child: const Text('Retour'), ), ], ), ), ); case AnalysisState.success: return _buildSuccessContent(context, provider); } }, ), floatingActionButton: Consumer( builder: (context, provider, _) { if (provider.state != AnalysisState.success) return const SizedBox.shrink(); if (_isCalibrating) { return FloatingActionButton.extended( onPressed: () { setState(() => _isCalibrating = false); }, backgroundColor: AppTheme.successColor, icon: const Icon(Icons.check), label: const Text('Valider'), ); } return FloatingActionButton.extended( onPressed: () => _saveSession(context, provider), icon: const Icon(Icons.save), label: const Text('Sauvegarder'), ); }, ), ); } Widget _buildSuccessContent(BuildContext context, AnalysisProvider provider) { return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Calibration mode indicator if (_isCalibrating) Container( color: AppTheme.warningColor, padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), child: const Row( children: [ Icon(Icons.tune, color: Colors.white, size: 20), SizedBox(width: 8), Expanded( child: Text( 'Mode Calibration - Ajustez le centre et la taille de la cible', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), ), ], ), ), // Reference selection mode indicator if (_isSelectingReferences) Container( color: Colors.deepPurple, padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), child: Row( children: [ const Icon(Icons.touch_app, color: Colors.white, size: 20), const SizedBox(width: 8), Expanded( child: Text( 'Selectionnez ${provider.referenceImpacts.length}/3-4 impacts de reference', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), ), if (provider.referenceImpacts.isNotEmpty) TextButton( onPressed: () => provider.clearReferenceImpacts(), child: const Text('Effacer', style: TextStyle(color: Colors.white)), ), ], ), ), // Calibration sliders (shown only in calibration mode) if (_isCalibrating) Container( color: Colors.black87, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Column( children: [ // Ring count slider Row( children: [ const Icon(Icons.radio_button_unchecked, color: Colors.white, size: 20), const SizedBox(width: 8), const Text('Anneaux:', style: TextStyle(color: Colors.white)), Expanded( child: Slider( value: provider.ringCount.toDouble(), min: 3, max: 12, divisions: 9, label: '${provider.ringCount}', activeColor: AppTheme.primaryColor, onChanged: (value) { provider.adjustTargetPosition( provider.targetCenterX, provider.targetCenterY, provider.targetRadius, ringCount: value.round(), ); }, ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( color: AppTheme.primaryColor, borderRadius: BorderRadius.circular(12), ), child: Text( '${provider.ringCount}', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), ), ], ), // Target size slider Row( children: [ const Icon(Icons.zoom_out_map, color: Colors.white, size: 20), const SizedBox(width: 8), const Text('Taille:', style: TextStyle(color: Colors.white)), Expanded( child: Slider( value: provider.targetRadius.clamp(0.05, 3.0), min: 0.05, max: 3.0, label: '${(provider.targetRadius * 100).toStringAsFixed(0)}%', activeColor: AppTheme.warningColor, onChanged: (value) { provider.adjustTargetPosition( provider.targetCenterX, provider.targetCenterY, value, ringCount: provider.ringCount, ); }, ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( color: AppTheme.warningColor, borderRadius: BorderRadius.circular(12), ), child: Text( '${(provider.targetRadius * 100).toStringAsFixed(0)}%', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), ), ], ), ], ), ), // Target image with overlay or calibration AspectRatio( aspectRatio: provider.imageAspectRatio, child: Stack( fit: StackFit.expand, children: [ Image.file( File(provider.imagePath!), fit: BoxFit.fill, ), if (_isCalibrating) TargetCalibration( initialCenterX: provider.targetCenterX, initialCenterY: provider.targetCenterY, initialRadius: provider.targetRadius, initialRingCount: provider.ringCount, initialRingRadii: provider.ringRadii, targetType: provider.targetType!, onCalibrationChanged: (centerX, centerY, radius, ringCount, {List? ringRadii}) { provider.adjustTargetPosition(centerX, centerY, radius, ringCount: ringCount, ringRadii: ringRadii); }, ) else TargetOverlay( shots: provider.shots, targetCenterX: provider.targetCenterX, targetCenterY: provider.targetCenterY, targetRadius: provider.targetRadius, targetType: provider.targetType!, ringCount: provider.ringCount, ringRadii: provider.ringRadii, onShotTapped: (shot) => _isSelectingReferences ? null : _showShotOptions(context, provider, shot.id), onAddShot: (x, y) { if (_isSelectingReferences) { provider.addReferenceImpact(x, y); } else { provider.addShot(x, y); } }, groupingCenterX: provider.groupingResult?.centerX, groupingCenterY: provider.groupingResult?.centerY, groupingDiameter: provider.groupingResult?.diameter, referenceImpacts: _isSelectingReferences ? provider.referenceImpacts : null, ), ], ), ), // Info cards (hidden during calibration) if (!_isCalibrating) Padding( padding: const EdgeInsets.all(AppConstants.defaultPadding), child: Column( children: [ // Calibration button Card( color: AppTheme.primaryColor.withValues(alpha: 0.1), child: ListTile( leading: const Icon(Icons.tune, color: AppTheme.primaryColor), title: const Text('Calibrer la cible'), subtitle: const Text('Ajustez le centre et la taille'), trailing: const Icon(Icons.arrow_forward_ios, size: 16), onTap: () => setState(() => _isCalibrating = true), ), ), const SizedBox(height: 12), // Score card ScoreCard( totalScore: provider.totalScore, shotCount: provider.shotCount, scoreResult: provider.scoreResult, targetType: provider.targetType!, ), const SizedBox(height: 12), // Grouping stats if (provider.groupingResult != null && provider.shotCount > 1) GroupingStats( groupingResult: provider.groupingResult!, targetCenterX: provider.targetCenterX, targetCenterY: provider.targetCenterY, ), const SizedBox(height: 12), // Action buttons _buildActionButtons(context, provider), ], ), ) else // Calibration info Padding( padding: const EdgeInsets.all(AppConstants.defaultPadding), child: Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Instructions de calibration', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), const SizedBox(height: 12), _buildInstructionItem( Icons.open_with, 'Glissez le centre (croix bleue) pour positionner le centre de la cible', ), _buildInstructionItem( Icons.zoom_out_map, 'Glissez le bord (cercle orange) pour ajuster la taille de la cible', ), _buildInstructionItem( Icons.visibility, 'Les zones de score sont affichees en transparence', ), const SizedBox(height: 12), Row( children: [ const Text('Centre: '), Text( '(${(provider.targetCenterX * 100).toStringAsFixed(1)}%, ${(provider.targetCenterY * 100).toStringAsFixed(1)}%)', style: const TextStyle(fontWeight: FontWeight.bold), ), ], ), Row( children: [ const Text('Rayon: '), Text( '${(provider.targetRadius * 100).toStringAsFixed(1)}%', style: const TextStyle(fontWeight: FontWeight.bold), ), ], ), ], ), ), ), ), ], ), ); } Widget _buildInstructionItem(IconData icon, String text) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, size: 20, color: AppTheme.primaryColor), const SizedBox(width: 8), Expanded(child: Text(text)), ], ), ); } Widget _buildActionButtons(BuildContext context, AnalysisProvider provider) { return Column( children: [ // Reference-based detection section if (_isSelectingReferences) ...[ Card( color: Colors.deepPurple.withValues(alpha: 0.1), child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.touch_app, color: Colors.deepPurple), const SizedBox(width: 8), Text( '${provider.referenceImpacts.length} reference(s) selectionnee(s)', style: const TextStyle(fontWeight: FontWeight.bold), ), ], ), const SizedBox(height: 8), const Text( 'Touchez l\'image pour marquer 3-4 impacts de reference. ' 'L\'algorithme apprendra leurs caracteristiques pour detecter les autres.', style: TextStyle(fontSize: 12, color: Colors.grey), ), const SizedBox(height: 12), Row( children: [ Expanded( child: OutlinedButton( onPressed: () { setState(() => _isSelectingReferences = false); provider.clearReferenceImpacts(); }, child: const Text('Annuler'), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton.icon( onPressed: provider.referenceImpacts.length >= 2 ? () => _showCalibratedDetectionDialog(context, provider) : null, icon: const Icon(Icons.auto_fix_high), label: const Text('Detecter'), style: ElevatedButton.styleFrom( backgroundColor: Colors.deepPurple, foregroundColor: Colors.white, ), ), ), ], ), ], ), ), ), const SizedBox(height: 12), ] else ...[ // Auto-detect buttons row Row( children: [ Expanded( child: ElevatedButton.icon( onPressed: () => _showAutoDetectDialog(context, provider), icon: const Icon(Icons.auto_fix_high), label: const Text('Auto-Detection'), style: ElevatedButton.styleFrom( backgroundColor: AppTheme.primaryColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 12), ), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton.icon( onPressed: () => setState(() => _isSelectingReferences = true), icon: const Icon(Icons.touch_app), label: const Text('Par Reference'), style: ElevatedButton.styleFrom( backgroundColor: Colors.deepPurple, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 12), ), ), ), ], ), const SizedBox(height: 12), ], // Manual actions Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: _isSelectingReferences ? null : () => _showAddShotHint(context), icon: const Icon(Icons.add_circle_outline), label: const Text('Ajouter'), ), ), const SizedBox(width: 12), Expanded( child: OutlinedButton.icon( onPressed: provider.shotCount > 0 && !_isSelectingReferences ? () => _showClearConfirmation(context, provider) : null, icon: const Icon(Icons.clear_all), label: const Text('Effacer'), ), ), ], ), ], ); } void _showHelpDialog(BuildContext context) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Aide'), content: const Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('1. Calibrez d\'abord la cible en utilisant le bouton de calibration'), SizedBox(height: 8), Text('2. Appuyez sur l\'image pour ajouter un impact manuellement'), SizedBox(height: 8), Text('3. Appuyez sur un impact pour le modifier ou le supprimer'), SizedBox(height: 8), Text('4. Les cercles bleus indiquent le groupement de vos tirs'), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('OK'), ), ], ), ); } void _showShotOptions(BuildContext context, AnalysisProvider provider, String shotId) { showModalBottomSheet( context: context, builder: (context) => SafeArea( child: Wrap( children: [ ListTile( leading: const Icon(Icons.delete, color: AppTheme.errorColor), title: const Text('Supprimer cet impact'), onTap: () { provider.removeShot(shotId); Navigator.pop(context); }, ), ], ), ), ); } void _showAddShotHint(BuildContext context) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Appuyez sur l\'image pour ajouter un impact'), duration: Duration(seconds: 2), ), ); } void _showClearConfirmation(BuildContext context, AnalysisProvider provider) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Confirmer'), content: const Text('Voulez-vous effacer tous les impacts?'), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Annuler'), ), TextButton( onPressed: () { for (final shot in List.from(provider.shots)) { provider.removeShot(shot.id); } Navigator.pop(context); }, child: const Text('Effacer', style: TextStyle(color: AppTheme.errorColor)), ), ], ), ); } void _showAutoDetectDialog(BuildContext context, AnalysisProvider provider) { int darkThreshold = 80; int minImpactSize = 20; int maxImpactSize = 500; double minCircularity = 0.6; double minFillRatio = 0.5; bool clearExisting = true; showDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setState) => AlertDialog( title: const Row( children: [ Icon(Icons.auto_fix_high, color: AppTheme.primaryColor), SizedBox(width: 8), Text('Auto-Detection'), ], ), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Ajustez les parametres de detection:', style: TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 16), // Dark threshold slider Text('Seuil de detection (zones sombres): $darkThreshold'), Slider( value: darkThreshold.toDouble(), min: 20, max: 150, divisions: 13, label: '$darkThreshold', onChanged: (value) { setState(() => darkThreshold = value.round()); }, ), const Text( 'Plus bas = detecte uniquement les zones tres sombres', style: TextStyle(fontSize: 12, color: Colors.grey), ), const SizedBox(height: 12), // Circularity slider Text('Circularite minimum: ${(minCircularity * 100).toStringAsFixed(0)}%'), Slider( value: minCircularity, min: 0.3, max: 0.9, divisions: 12, label: '${(minCircularity * 100).toStringAsFixed(0)}%', onChanged: (value) { setState(() => minCircularity = value); }, ), const Text( 'Plus haut = detecte uniquement les formes rondes', style: TextStyle(fontSize: 12, color: Colors.grey), ), const SizedBox(height: 12), // Fill ratio slider Text('Remplissage minimum: ${(minFillRatio * 100).toStringAsFixed(0)}%'), Slider( value: minFillRatio, min: 0.3, max: 0.9, divisions: 12, label: '${(minFillRatio * 100).toStringAsFixed(0)}%', onChanged: (value) { setState(() => minFillRatio = value); }, ), const Text( 'Plus haut = detecte les trous pleins (evite les cercles creux des chiffres)', style: TextStyle(fontSize: 12, color: Colors.grey), ), const SizedBox(height: 12), // Min impact size slider Text('Taille minimum (pixels): $minImpactSize'), Slider( value: minImpactSize.toDouble(), min: 5, max: 100, divisions: 19, label: '$minImpactSize', onChanged: (value) { setState(() => minImpactSize = value.round()); }, ), const SizedBox(height: 12), // Max impact size slider Text('Taille maximum (pixels): $maxImpactSize'), Slider( value: maxImpactSize.toDouble(), min: 100, max: 1000, divisions: 18, label: '$maxImpactSize', onChanged: (value) { setState(() => maxImpactSize = value.round()); }, ), const SizedBox(height: 12), // Clear existing checkbox CheckboxListTile( title: const Text('Effacer les impacts existants'), value: clearExisting, onChanged: (value) { setState(() => clearExisting = value ?? true); }, contentPadding: EdgeInsets.zero, ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Annuler'), ), ElevatedButton.icon( onPressed: () async { Navigator.pop(context); // Show loading indicator ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Row( children: [ SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ), SizedBox(width: 12), Text('Detection en cours...'), ], ), duration: Duration(seconds: 10), ), ); // Run detection final count = await provider.autoDetectImpacts( darkThreshold: darkThreshold, minImpactSize: minImpactSize, maxImpactSize: maxImpactSize, minCircularity: minCircularity, minFillRatio: minFillRatio, clearExisting: clearExisting, ); // Hide loading and show result if (context.mounted) { ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( count > 0 ? '$count impact(s) detecte(s)' : 'Aucun impact detecte. Essayez d\'ajuster les parametres.', ), backgroundColor: count > 0 ? AppTheme.successColor : AppTheme.warningColor, ), ); } }, icon: const Icon(Icons.search), label: const Text('Detecter'), ), ], ), ), ); } void _showCalibratedDetectionDialog(BuildContext context, AnalysisProvider provider) { double tolerance = 2.0; bool clearExisting = true; showDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setState) => AlertDialog( title: const Row( children: [ Icon(Icons.touch_app, color: Colors.deepPurple), SizedBox(width: 8), Text('Detection par Reference'), ], ), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Card( color: Colors.deepPurple.withValues(alpha: 0.1), child: Padding( padding: const EdgeInsets.all(12), child: Row( children: [ const Icon(Icons.info_outline, color: Colors.deepPurple), const SizedBox(width: 8), Expanded( child: Text( '${provider.referenceImpacts.length} impacts de reference selectionnes', style: const TextStyle(fontWeight: FontWeight.bold), ), ), ], ), ), ), const SizedBox(height: 16), const Text( 'L\'algorithme va analyser les caracteristiques des impacts ' 'de reference (luminosite, taille, forme) et chercher des ' 'impacts similaires sur toute l\'image.', style: TextStyle(fontSize: 13), ), const SizedBox(height: 16), // Tolerance slider Text('Tolerance: ${tolerance.toStringAsFixed(1)}x'), Slider( value: tolerance, min: 1.0, max: 5.0, divisions: 8, label: '${tolerance.toStringAsFixed(1)}x', activeColor: Colors.deepPurple, onChanged: (value) { setState(() => tolerance = value); }, ), const Text( 'Tolerance basse = detection stricte (moins de faux positifs)\n' 'Tolerance haute = detection large (plus de detections)', style: TextStyle(fontSize: 12, color: Colors.grey), ), const SizedBox(height: 12), // Clear existing checkbox CheckboxListTile( title: const Text('Effacer les impacts existants'), value: clearExisting, activeColor: Colors.deepPurple, onChanged: (value) { setState(() => clearExisting = value ?? true); }, contentPadding: EdgeInsets.zero, ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Annuler'), ), ElevatedButton.icon( onPressed: () async { Navigator.pop(context); // Learn from references ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Row( children: [ SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ), SizedBox(width: 12), Text('Apprentissage des references...'), ], ), duration: Duration(seconds: 10), ), ); final learned = provider.learnFromReferences(); if (!learned) { if (context.mounted) { ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Impossible d\'analyser les references. Essayez de selectionner d\'autres impacts.'), backgroundColor: AppTheme.errorColor, ), ); } return; } // Run calibrated detection if (context.mounted) { ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Row( children: [ SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), ), SizedBox(width: 12), Text('Detection en cours...'), ], ), duration: Duration(seconds: 10), ), ); } final count = await provider.detectFromReferences( tolerance: tolerance, clearExisting: clearExisting, ); // Exit reference mode if (mounted) { setState(() {}); } _isSelectingReferences = false; provider.clearReferenceImpacts(); // Show result if (context.mounted) { ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( count > 0 ? '$count impact(s) detecte(s) a partir des references' : 'Aucun impact detecte. Essayez d\'augmenter la tolerance.', ), backgroundColor: count > 0 ? AppTheme.successColor : AppTheme.warningColor, ), ); } // Trigger rebuild if (mounted) { this.setState(() {}); } }, icon: const Icon(Icons.search), label: const Text('Detecter'), style: ElevatedButton.styleFrom( backgroundColor: Colors.deepPurple, foregroundColor: Colors.white, ), ), ], ), ), ); } Future _saveSession(BuildContext context, AnalysisProvider provider) async { final notesController = TextEditingController(); final shouldSave = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Sauvegarder la session'), content: TextField( controller: notesController, decoration: const InputDecoration( labelText: 'Notes (optionnel)', hintText: 'Ex: Entrainement, 10m, debout...', ), maxLines: 2, ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Annuler'), ), TextButton( onPressed: () => Navigator.pop(context, true), child: const Text('Sauvegarder'), ), ], ), ); if (shouldSave == true) { try { await provider.saveSession(notes: notesController.text.isEmpty ? null : notesController.text); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Session sauvegardee'), backgroundColor: AppTheme.successColor, ), ); Navigator.pop(context); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur: $e'), backgroundColor: AppTheme.errorColor, ), ); } } } } }