727 lines
27 KiB
Dart
727 lines
27 KiB
Dart
/// Écran principal de Plotting et d'analyse - Interface centrale de traitement des cibles.
|
|
///
|
|
/// Affiche d'abord la calibration de la cible, puis l'overlay des anneaux et impacts détectés.
|
|
/// Permet le calcul des scores et statistiques de groupement (Plotting).
|
|
library;
|
|
|
|
import 'dart:io';
|
|
import 'dart:math' as math;
|
|
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/models/shot.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 '../session/session_provider.dart';
|
|
import 'analysis_provider.dart';
|
|
import '../crop/crop_screen.dart';
|
|
import '../capture/capture_screen.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;
|
|
// CORRECTION : Utilisation de coordonnées directes pour effacer l'erreur rouge
|
|
final double? initialCenterX;
|
|
final double? initialCenterY;
|
|
final double? cropScale;
|
|
final Offset? cropOffset;
|
|
|
|
const AnalysisScreen({
|
|
super.key,
|
|
required this.imagePath,
|
|
required this.targetType,
|
|
this.initialCenterX,
|
|
this.initialCenterY,
|
|
this.cropScale,
|
|
this.cropOffset,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// Reconstitution de l'Offset si présent pour le provider en arrière-plan
|
|
final manualCenterOffset = (initialCenterX != null && initialCenterY != null)
|
|
? Offset(initialCenterX!, initialCenterY!)
|
|
: null;
|
|
|
|
return ChangeNotifierProvider(
|
|
create: (context) => AnalysisProvider(
|
|
detectionService: context.read<TargetDetectionService>(),
|
|
scoreCalculatorService: context.read<ScoreCalculatorService>(),
|
|
groupingAnalyzerService: context.read<GroupingAnalyzerService>(),
|
|
sessionRepository: context.read<SessionRepository>(),
|
|
)..analyzeImage(imagePath, targetType, autoAnalyze: false, manualCenter: manualCenterOffset),
|
|
child: _AnalysisScreenContent(
|
|
cropScale: cropScale,
|
|
cropOffset: cropOffset,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AnalysisScreenContent extends StatefulWidget {
|
|
final double? cropScale;
|
|
final Offset? cropOffset;
|
|
|
|
const _AnalysisScreenContent({
|
|
this.cropScale,
|
|
this.cropOffset,
|
|
});
|
|
|
|
@override
|
|
State<_AnalysisScreenContent> createState() => _AnalysisScreenContentState();
|
|
}
|
|
|
|
class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|
final GlobalKey<TargetCalibrationState> _calibrationKey = GlobalKey<TargetCalibrationState>();
|
|
|
|
// CORRECTION : Forcé à TRUE pour arriver directement sur la calibration après le Crop !
|
|
bool _isCalibrating = true;
|
|
|
|
bool _isSelectingReferences = false;
|
|
bool _isAtBottom = false;
|
|
final ScrollController _scrollController = ScrollController();
|
|
final TransformationController _transformationController = TransformationController();
|
|
final GlobalKey _imageKey = GlobalKey();
|
|
double _currentZoomScale = 1.0;
|
|
|
|
// AJOUT : Stockage de l'ID du tir en cours de déplacement par appui long
|
|
String? _movingShotId;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_transformationController.addListener(_onTransformChanged);
|
|
_scrollController.addListener(_onScroll);
|
|
}
|
|
|
|
void _onScroll() {
|
|
if (!_scrollController.hasClients) return;
|
|
final isBottom =
|
|
_scrollController.position.pixels >=
|
|
_scrollController.position.maxScrollExtent - 20;
|
|
if (isBottom != _isAtBottom) {
|
|
setState(() {
|
|
_isAtBottom = isBottom;
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_transformationController.removeListener(_onTransformChanged);
|
|
_transformationController.dispose();
|
|
_scrollController.removeListener(_onScroll);
|
|
_scrollController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _onTransformChanged() {
|
|
final scale = _transformationController.value.getMaxScaleOnAxis();
|
|
if (scale != _currentZoomScale) {
|
|
setState(() {
|
|
_currentZoomScale = scale;
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final provider = context.watch<AnalysisProvider>();
|
|
final sessionProvider = context.watch<SessionProvider>();
|
|
final targetNumber = sessionProvider.isSessionActive ? sessionProvider.targetCount + 1 : null;
|
|
|
|
// CORRECTION : Remplacement du mot "Analyse" par "Plotting"
|
|
final titlePrefix = _isCalibrating ? 'Calibration' : 'Plotting';
|
|
final title = targetNumber != null ? '$titlePrefix - Cible $targetNumber' : ( _isCalibrating ? 'Calibration' : 'Plotting du Tir');
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(title),
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_back),
|
|
onPressed: () {
|
|
if (_isCalibrating) {
|
|
// Si on fait retour pendant la calibration, on retourne sagement au CropScreen
|
|
final provider = context.read<AnalysisProvider>();
|
|
Navigator.pushReplacement(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => CropScreen(
|
|
imagePath: provider.imagePath!,
|
|
targetType: provider.targetType!,
|
|
initialScale: widget.cropScale,
|
|
initialOffset: widget.cropOffset,
|
|
),
|
|
),
|
|
);
|
|
} else {
|
|
// Si on fait retour depuis le Plotting, on revient en arrière dans la calibration
|
|
setState(() => _isCalibrating = true);
|
|
}
|
|
},
|
|
),
|
|
actions: [
|
|
if (!_isCalibrating && !_isSelectingReferences)
|
|
IconButton(
|
|
icon: const Icon(Icons.refresh),
|
|
onPressed: () => provider.analyzeImage(context.read<AnalysisProvider>().imagePath!, context.read<AnalysisProvider>().targetType!),
|
|
),
|
|
if (_isCalibrating)
|
|
TextButton(
|
|
onPressed: () => setState(() => _isCalibrating = false), // Dévoile le Plotting Screen
|
|
child: const Text(
|
|
'TERMINER',
|
|
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
body: Stack(
|
|
children: [
|
|
Navigator.canPop(context) ? const SizedBox.shrink() : const SizedBox.shrink(),
|
|
SingleChildScrollView(
|
|
controller: _scrollController,
|
|
child: Column(
|
|
children: [
|
|
// Header de chargement
|
|
Padding(
|
|
padding: const EdgeInsets.all(AppConstants.defaultPadding),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (provider.state == AnalysisState.loading)
|
|
const Center(
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 8.0),
|
|
child: CircularProgressIndicator(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Zone centrale d'affichage : Calibration OU Plotting interactif
|
|
AspectRatio(
|
|
aspectRatio: provider.imageAspectRatio,
|
|
child: _isCalibrating
|
|
? Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
Image.file(
|
|
File(provider.imagePath!),
|
|
fit: BoxFit.fill,
|
|
),
|
|
TargetCalibration(
|
|
key: _calibrationKey,
|
|
initialCenterX: provider.targetCenterX,
|
|
initialCenterY: provider.targetCenterY,
|
|
initialRadius: provider.targetRadius,
|
|
initialInnerRadius: provider.targetInnerRadius,
|
|
initialRingCount: provider.ringCount,
|
|
initialRingRadii: provider.ringRadii,
|
|
targetType: provider.targetType!,
|
|
onCalibrationChanged:
|
|
(
|
|
centerX,
|
|
centerY,
|
|
innerRadius,
|
|
radius,
|
|
ringCount, {
|
|
List<double>? ringRadii,
|
|
}) {
|
|
provider.adjustTargetPosition(
|
|
centerX,
|
|
centerY,
|
|
innerRadius,
|
|
radius,
|
|
ringCount: ringCount,
|
|
ringRadii: ringRadii,
|
|
);
|
|
},
|
|
),
|
|
],
|
|
)
|
|
: _buildZoomableImageWithOverlay(context, provider),
|
|
),
|
|
|
|
// Vue inférieure : Cartes de scores (Plotting) OU Flèches de réglages (Calibration)
|
|
if (!_isCalibrating)
|
|
Padding(
|
|
padding: const EdgeInsets.all(AppConstants.defaultPadding),
|
|
child: Column(
|
|
children: [
|
|
// Petit bouton pour réajuster la calibration si besoin
|
|
Card(
|
|
color: AppTheme.primaryColor.withValues(alpha: 0.1),
|
|
child: ListTile(
|
|
leading: const Icon(
|
|
Icons.tune,
|
|
color: AppTheme.primaryColor,
|
|
),
|
|
title: const Text('Ajuster la calibration'),
|
|
subtitle: const Text(
|
|
'Modifier le centre ou le rayon global',
|
|
),
|
|
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),
|
|
_buildActionButtons(context, provider),
|
|
const SizedBox(height: 50),
|
|
],
|
|
),
|
|
)
|
|
else
|
|
// Mode Calibration actif : On affiche les flèches de micro-ajustement
|
|
Padding(
|
|
padding: const EdgeInsets.all(AppConstants.defaultPadding),
|
|
child: Column(
|
|
children: [
|
|
const Text(
|
|
'Ajustement precis (pixel par pixel)',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.white70,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Center(
|
|
child: Builder(
|
|
builder: (context) {
|
|
final size = MediaQuery.of(context).size;
|
|
return _calibrationKey.currentState?.buildDirectionalControls(context, size)
|
|
?? const SizedBox.shrink();
|
|
}
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
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 pour positionner le centre de la cible',
|
|
),
|
|
_buildInstructionItem(
|
|
Icons.zoom_out_map,
|
|
'Pincez l\'ecran a 2 doigts ou utilisez la jauge pour la taille',
|
|
),
|
|
_buildInstructionItem(
|
|
Icons.visibility,
|
|
'Appuyez sur TERMINER en haut a droite pour valider',
|
|
),
|
|
const SizedBox(height: 16),
|
|
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),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Positioned(
|
|
bottom: 0,
|
|
left: 0,
|
|
right: 0,
|
|
child: Align(
|
|
alignment: _isAtBottom
|
|
? Alignment.bottomCenter
|
|
: Alignment.bottomRight,
|
|
child: Padding(
|
|
padding: _isAtBottom
|
|
? EdgeInsets.zero
|
|
: const EdgeInsets.all(16.0),
|
|
child: _isCalibrating
|
|
? const SizedBox.shrink()
|
|
: FloatingActionButton.extended(
|
|
onPressed: () => _showSaveSessionDialog(context, provider),
|
|
backgroundColor: AppTheme.primaryColor,
|
|
icon: const Icon(Icons.save),
|
|
label: const Text('TERMINER LA SESSION'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildInstructionItem(IconData icon, String text) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
child: Row(
|
|
children: [
|
|
Icon(icon, size: 16, color: AppTheme.primaryColor),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
text,
|
|
style: const TextStyle(fontSize: 13),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildZoomableImageWithOverlay(
|
|
BuildContext context,
|
|
AnalysisProvider provider,
|
|
) {
|
|
return InteractiveViewer(
|
|
transformationController: _transformationController,
|
|
minScale: 1.0,
|
|
maxScale: 10.0,
|
|
boundaryMargin: const EdgeInsets.all(double.infinity),
|
|
// Désactive le pan à deux doigts quand on déplace un impact pour éviter les tremblements d'image
|
|
panEnabled: _movingShotId == null,
|
|
child: GestureDetector(
|
|
// 1. CAPTURE DU DOUBLE-TAP (Ouverture directe de l'édition)
|
|
onDoubleTapDown: (TapDownDetails details) {
|
|
final RenderBox? box = _imageKey.currentContext?.findRenderObject() as RenderBox?;
|
|
if (box == null) return;
|
|
|
|
final localOffset = box.globalToLocal(details.globalPosition);
|
|
final relX = localOffset.dx / box.size.width;
|
|
final relY = localOffset.dy / box.size.height;
|
|
|
|
if (provider.shots.isEmpty) return;
|
|
|
|
Shot? closestShot;
|
|
double minDistance = double.infinity;
|
|
const double clickTolerance = 0.05;
|
|
|
|
for (final shot in provider.shots) {
|
|
final dx = shot.x - relX;
|
|
final dy = shot.y - relY;
|
|
final distance = math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (distance < minDistance && distance < clickTolerance) {
|
|
minDistance = distance;
|
|
closestShot = shot;
|
|
}
|
|
}
|
|
|
|
if (closestShot != null) {
|
|
_showShotDetails(context, provider, closestShot);
|
|
}
|
|
},
|
|
|
|
// 2. APPUI LONG : Début de sélection de l'impact à déplacer
|
|
onLongPressStart: (LongPressStartDetails details) {
|
|
final RenderBox? box = _imageKey.currentContext?.findRenderObject() as RenderBox?;
|
|
if (box == null) return;
|
|
|
|
final localOffset = box.globalToLocal(details.globalPosition);
|
|
final relX = localOffset.dx / box.size.width;
|
|
final relY = localOffset.dy / box.size.height;
|
|
|
|
if (provider.shots.isEmpty) return;
|
|
|
|
Shot? closestShot;
|
|
double minDistance = double.infinity;
|
|
const double dragTolerance = 0.06; // Tolérance d'accroche un peu plus large pour le doigt
|
|
|
|
for (final shot in provider.shots) {
|
|
final dx = shot.x - relX;
|
|
final dy = shot.y - relY;
|
|
final distance = math.sqrt(dx * dx + dy * dy);
|
|
|
|
if (distance < minDistance && distance < dragTolerance) {
|
|
minDistance = distance;
|
|
closestShot = shot;
|
|
}
|
|
}
|
|
|
|
// Si on trouve un impact, on active un petit vibreur (si dispo de base) et on verrouille son ID
|
|
if (closestShot != null) {
|
|
setState(() {
|
|
_movingShotId = closestShot!.id;
|
|
});
|
|
}
|
|
},
|
|
|
|
// 3. GLISSER : On déplace l'impact verrouillé en temps réel sous le doigt
|
|
onLongPressMoveUpdate: (LongPressMoveUpdateDetails details) {
|
|
if (_movingShotId == null) return;
|
|
|
|
final RenderBox? box = _imageKey.currentContext?.findRenderObject() as RenderBox?;
|
|
if (box == null) return;
|
|
|
|
final localOffset = box.globalToLocal(details.globalPosition);
|
|
|
|
// Calcul des coordonnées relatives bridées entre 0.0 et 1.0 (pour pas sortir de la photo)
|
|
final relX = (localOffset.dx / box.size.width).clamp(0.0, 1.0);
|
|
final relY = (localOffset.dy / box.size.height).clamp(0.0, 1.0);
|
|
|
|
// On met à jour directement la position dans le state global via le provider
|
|
provider.updateShotPosition(_movingShotId!, relX, relY);
|
|
},
|
|
|
|
// 4. RELÂCHER : Fin du déplacement de l'impact
|
|
onLongPressEnd: (_) {
|
|
if (_movingShotId != null) {
|
|
setState(() {
|
|
_movingShotId = null; // Libère le verrou
|
|
});
|
|
}
|
|
},
|
|
child: Stack(
|
|
children: [
|
|
Image.file(
|
|
File(provider.imagePath!),
|
|
key: _imageKey,
|
|
fit: BoxFit.fill,
|
|
),
|
|
TargetOverlay(
|
|
targetCenterX: provider.targetCenterX,
|
|
targetCenterY: provider.targetCenterY,
|
|
targetRadius: provider.targetRadius,
|
|
targetType: provider.targetType!,
|
|
shots: provider.shots,
|
|
showRings: true,
|
|
zoomScale: _currentZoomScale,
|
|
onShotTapped: (shot) => _showShotDetails(context, provider, shot),
|
|
onAddShot: (relX, relY) => provider.addShot(relX, relY),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildActionButtons(BuildContext context, AnalysisProvider provider) {
|
|
return const Column(
|
|
children: [
|
|
Row(
|
|
children: [],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
void _showShotDetails(
|
|
BuildContext context,
|
|
AnalysisProvider provider,
|
|
Shot shot,
|
|
) {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
builder: (context) => Container(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
'Impact #${provider.shots.indexOf(shot) + 1}',
|
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
|
),
|
|
Text(
|
|
'ID: ${shot.id}',
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.grey, fontSize: 10),
|
|
),
|
|
const SizedBox(height: 16),
|
|
ListTile(
|
|
leading: const Icon(Icons.score),
|
|
title: const Text('Modifier le score'),
|
|
trailing: DropdownButton<int>(
|
|
value: shot.score.clamp(0, 10),
|
|
items: List.generate(11, (index) => index)
|
|
.map((s) => DropdownMenuItem(
|
|
value: s,
|
|
child: Text('$s', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
|
|
))
|
|
.toList(),
|
|
onChanged: (newScore) {
|
|
if (newScore != null) {
|
|
provider.updateShotScore(shot.id, newScore);
|
|
Navigator.pop(context);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton.icon(
|
|
onPressed: () {
|
|
provider.removeShot(shot.id);
|
|
Navigator.pop(context);
|
|
},
|
|
icon: const Icon(Icons.delete, color: Colors.red),
|
|
label: const Text('SUPPRIMER', style: TextStyle(color: Colors.red)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showSaveSessionDialog(BuildContext context, AnalysisProvider provider) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Session Terminee'),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Nombre de tirs: ${provider.shotCount}'),
|
|
Text('Score total: ${provider.totalScore}'),
|
|
const SizedBox(height: 16),
|
|
const Text('Voulez-vous enregistrer cette session ?'),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () {
|
|
Navigator.pop(context);
|
|
final path = provider.imagePath!;
|
|
final type = provider.targetType!;
|
|
|
|
Navigator.pushReplacement(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => CropScreen(
|
|
imagePath: path,
|
|
targetType: type,
|
|
initialScale: widget.cropScale,
|
|
initialOffset: widget.cropOffset,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
child: const Text('ANNULER'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () async {
|
|
try {
|
|
final sessionProvider = context.read<SessionProvider>();
|
|
final analysis = await provider.saveSession(
|
|
sessionId: sessionProvider.activeSessionId,
|
|
weaponName: sessionProvider.currentWeapon,
|
|
weaponId: sessionProvider.currentWeaponId,
|
|
distance: sessionProvider.distance,
|
|
);
|
|
|
|
sessionProvider.addAnalysis(analysis);
|
|
|
|
if (context.mounted) {
|
|
Navigator.pop(context);
|
|
Navigator.pushAndRemoveUntil(
|
|
context,
|
|
MaterialPageRoute(builder: (context) => const CaptureScreen()),
|
|
(route) => route.isFirst,
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Erreur: $e'), backgroundColor: AppTheme.errorColor),
|
|
);
|
|
}
|
|
}
|
|
},
|
|
child: const Text('AJOUTER UNE CIBLE'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () async {
|
|
try {
|
|
final sessionProvider = context.read<SessionProvider>();
|
|
await provider.saveSession(
|
|
sessionId: sessionProvider.activeSessionId,
|
|
weaponName: sessionProvider.currentWeapon,
|
|
weaponId: sessionProvider.currentWeaponId,
|
|
distance: sessionProvider.distance,
|
|
);
|
|
|
|
if (context.mounted) {
|
|
sessionProvider.endSession();
|
|
Navigator.pop(context);
|
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
|
}
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('Erreur: $e'), backgroundColor: AppTheme.errorColor),
|
|
);
|
|
}
|
|
}
|
|
},
|
|
child: const Text('TERMINER TOUT'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
} |