From cec9838e9b44d034fac3829b2cc97b67b782115c Mon Sep 17 00:00:00 2001 From: qguillaume Date: Sat, 4 Jul 2026 10:40:25 +0200 Subject: [PATCH] =?UTF-8?q?refactor:=20perf=20getAllSessions,=20z=C3=A9ro?= =?UTF-8?q?=20warning=20analyze,=20docs=20=C3=A0=20jour?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - getAllSessions : élimine le N+1 (une requête par session + par analyse) au profit de 3 requêtes groupées (sessions, analyses IN, shots IN), découpées en paquets de 500 pour rester sous la limite SQLite. Assemblage en mémoire via maps — accueil/historique/stats ne ralentiront plus au fil des mois - flutter analyze : 28 -> 0 problème. APIs dépréciées migrées (withOpacity -> withValues, scale -> scaleByDouble, DropdownButtonFormField.value -> initialValue, dialogBackgroundColor -> dialogTheme, activeColor -> activeThumbColor), use_build_context_synchronously corrigés dans l'armurerie (repository capturé avant await + gardes mounted), print -> debugPrint, identifiants TopLeft/... -> lowerCamelCase, blocs if, champ final - _showShotDetails dédupliqué : bottom sheet extraite dans le widget partagé shot_details_sheet.dart, utilisée par l'écran d'analyse et l'éditeur d'impacts - CLAUDE.md : sections Features réécrites (retrait détection auto/références, ajout capture/plotting manuel) ; description pubspec renseignée Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 19 ++--- lib/data/database/database_helper.dart | 70 +++++++++++++-- lib/features/analysis/analysis_screen.dart | 79 +---------------- .../analysis/impact_editor_screen.dart | 80 +---------------- .../analysis/widgets/shot_details_sheet.dart | 85 +++++++++++++++++++ lib/features/capture/capture_screen.dart | 36 ++++---- lib/features/crop/crop_screen.dart | 10 +-- lib/features/garage/weapon_detail_screen.dart | 47 +++++----- lib/features/garage/weapon_list_screen.dart | 20 +++-- lib/features/history/history_screen.dart | 2 +- .../session/session_setup_screen.dart | 2 +- lib/features/settings/settings_screen.dart | 4 +- .../statistics/statistics_screen.dart | 2 +- pubspec.yaml | 2 +- 14 files changed, 229 insertions(+), 229 deletions(-) create mode 100644 lib/features/analysis/widgets/shot_details_sheet.dart diff --git a/CLAUDE.md b/CLAUDE.md index 17f1ee14..ff8b75e6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,10 +41,12 @@ flutter test --coverage ## Features -### Analyse de cibles de tir +### Capture de cibles de tir - Support de cibles concentriques (anneaux) et silhouettes - Chargement d'images de cibles depuis la galerie ou la caméra -- Détection automatique du centre et du rayon de la cible +- Aperçu caméra avec aide au cadrage : détection OpenCV de la cible (mire) + et indicateur de parallélisme par accéléromètre (pitch/roll) +- Écran de centrage/recadrage : rotation fine, déplacement pixel par pixel ### Calibration des cibles - Ajustement manuel du centre, du rayon et du nombre d'anneaux (1-10) @@ -52,14 +54,11 @@ flutter test --coverage - Slider global pour redimensionner tous les anneaux proportionnellement - Visualisation en temps réel des zones de score -### Détection d'impacts -- **Ajout manuel** : cliquer sur l'image pour placer un impact -- **Détection automatique** : algorithme de détection de blobs avec paramètres ajustables - - Seuil de luminosité - - Taille min/max des impacts - - Circularité minimale - - Ratio de remplissage (distingue les trous pleins des cercles vides) -- **Détection par références** : sélectionner 2-4 impacts manuellement, l'algorithme apprend leurs caractéristiques et détecte les impacts similaires +### Placement des impacts +- **Éditeur d'impacts plein écran** : tap pour ajouter, tap sur un impact pour + l'éditer (score/suppression), appui long pour déplacer, pincer pour zoomer +- Le placement est entièrement manuel ; le bouton ↻ de l'écran de plotting + efface tous les impacts sans toucher à la calibration ### Calcul des scores - Score automatique basé sur la position de l'impact dans les zones diff --git a/lib/data/database/database_helper.dart b/lib/data/database/database_helper.dart index 5f285ec0..52681b6b 100644 --- a/lib/data/database/database_helper.dart +++ b/lib/data/database/database_helper.dart @@ -380,17 +380,71 @@ class DatabaseHelper { limit: limit, offset: offset, ); + if (sessionMaps.isEmpty) return []; - final sessions = []; - for (final sessionMap in sessionMaps) { - final sessionId = sessionMap['id'] as String; - final session = await getSession(sessionId); - if (session != null) { - sessions.add(session); - } + // 3 requêtes groupées au lieu d'une cascade par session puis par analyse + // (N+1) : l'ancien code refaisait un getSession complet pour chaque ligne, + // ce qui ralentissait l'accueil/historique/stats au fil des mois. + final sessionIds = sessionMaps.map((m) => m['id'] as String).toList(); + final analysisMaps = await _queryIn( + db, + AppConstants.targetAnalysesTable, + 'session_id', + sessionIds, + ); + final analysisIds = analysisMaps.map((m) => m['id'] as String).toList(); + final shotMaps = await _queryIn( + db, + AppConstants.shotsTable, + 'analysis_id', + analysisIds, + ); + + final shotsByAnalysis = >{}; + for (final map in shotMaps) { + (shotsByAnalysis[map['analysis_id'] as String] ??= []) + .add(Shot.fromMap(map)); } - return sessions; + final analysesBySession = >{}; + for (final map in analysisMaps) { + final analysis = TargetAnalysis.fromMap( + map, + shotsByAnalysis[map['id'] as String] ?? [], + ); + (analysesBySession[map['session_id'] as String] ??= []).add(analysis); + } + + return sessionMaps + .map((m) => + Session.fromMap(m, analysesBySession[m['id'] as String] ?? [])) + .toList(); + } + + /// SELECT * FROM [table] WHERE [column] IN (values), découpé par paquets + /// de 500 pour rester sous la limite de variables d'une requête SQLite (999). + Future>> _queryIn( + Database db, + String table, + String column, + List values, + ) async { + if (values.isEmpty) return []; + const chunkSize = 500; + final results = >[]; + for (var i = 0; i < values.length; i += chunkSize) { + final chunk = values.sublist( + i, + i + chunkSize > values.length ? values.length : i + chunkSize, + ); + final placeholders = List.filled(chunk.length, '?').join(','); + results.addAll(await db.query( + table, + where: '$column IN ($placeholders)', + whereArgs: chunk, + )); + } + return results; } Future deleteSession(String id) async { diff --git a/lib/features/analysis/analysis_screen.dart b/lib/features/analysis/analysis_screen.dart index c4df706a..dfc69516 100644 --- a/lib/features/analysis/analysis_screen.dart +++ b/lib/features/analysis/analysis_screen.dart @@ -12,7 +12,6 @@ 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/score_calculator_service.dart'; import '../../services/grouping_analyzer_service.dart'; @@ -26,6 +25,7 @@ import 'widgets/target_overlay.dart'; import 'widgets/target_calibration.dart'; import 'widgets/score_card.dart'; import 'widgets/grouping_stats.dart'; +import 'widgets/shot_details_sheet.dart'; class AnalysisScreen extends StatelessWidget { final String imagePath; @@ -364,7 +364,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { widget.cropOffset?.dy ?? 0.0, 0.0, ) - ..scale(1.0, 1.0) ..rotateZ( (provider.cropRotation) * (math.pi / 180), @@ -639,7 +638,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { zoomScale: _currentZoomScale, // Lecture seule : tap sur impact -> détails (consultation). onShotTapped: (shot) => - _showShotDetails(context, provider, shot), + showShotDetailsSheet(context, provider, shot), ), ), ], @@ -647,80 +646,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> { ); } - 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( - 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, diff --git a/lib/features/analysis/impact_editor_screen.dart b/lib/features/analysis/impact_editor_screen.dart index 4039c3a8..9d395651 100644 --- a/lib/features/analysis/impact_editor_screen.dart +++ b/lib/features/analysis/impact_editor_screen.dart @@ -23,6 +23,7 @@ import 'package:provider/provider.dart'; import '../../data/models/shot.dart'; import 'analysis_provider.dart'; import 'widgets/target_overlay.dart'; +import 'widgets/shot_details_sheet.dart'; class ImpactEditorScreen extends StatefulWidget { const ImpactEditorScreen({super.key}); @@ -144,7 +145,7 @@ class _ImpactEditorScreenState extends State { final hit = _hitTestShot(provider, rel); if (hit != null) { - _showShotDetails(context, provider, hit); + showShotDetailsSheet(context, provider, hit); } else { provider.addShot(rel.dx, rel.dy); } @@ -192,7 +193,7 @@ class _ImpactEditorScreenState extends State { // L'ajout et la sélection sont gérés par le // GestureDetector parent ci-dessus. onShotTapped: (shot) => - _showShotDetails(context, provider, shot), + showShotDetailsSheet(context, provider, shot), ), ), ], @@ -205,79 +206,4 @@ class _ImpactEditorScreenState extends State { ), ); } - - 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( - 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), - ), - ), - ), - ], - ), - ], - ), - ), - ); - } } \ No newline at end of file diff --git a/lib/features/analysis/widgets/shot_details_sheet.dart b/lib/features/analysis/widgets/shot_details_sheet.dart new file mode 100644 index 00000000..635f6a62 --- /dev/null +++ b/lib/features/analysis/widgets/shot_details_sheet.dart @@ -0,0 +1,85 @@ +/// Bottom sheet de détails d'un impact : modification du score et suppression. +/// +/// Partagée entre l'écran d'analyse (consultation du plotting) et l'éditeur +/// d'impacts plein écran, qui opèrent sur le même AnalysisProvider. +library; + +import 'package:flutter/material.dart'; + +import '../../../data/models/shot.dart'; +import '../analysis_provider.dart'; + +void showShotDetailsSheet( + 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( + 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), + ), + ), + ), + ], + ), + ], + ), + ), + ); +} diff --git a/lib/features/capture/capture_screen.dart b/lib/features/capture/capture_screen.dart index c7c71f84..0818ecea 100644 --- a/lib/features/capture/capture_screen.dart +++ b/lib/features/capture/capture_screen.dart @@ -305,7 +305,9 @@ class _CaptureScreenState extends State final now = DateTime.now(); // Cadence ~1 s : assez réactif pour suivre la cible sans saturer le CPU. if (lastAnalysis != null && - now.difference(lastAnalysis!).inMilliseconds < 1000) return; + now.difference(lastAnalysis!).inMilliseconds < 1000) { + return; + } lastAnalysis = now; _isAnalyzingFrame = true; @@ -514,10 +516,10 @@ class _CaptureScreenState extends State height: MediaQuery.of(context).size.width * 0.85, child: Stack( children: [ - _buildCameraCorner(TopLeft: true, color: frameColor), - _buildCameraCorner(TopRight: true, color: frameColor), - _buildCameraCorner(BottomLeft: true, color: frameColor), - _buildCameraCorner(BottomRight: true, color: frameColor), + _buildCameraCorner(topLeft: true, color: frameColor), + _buildCameraCorner(topRight: true, color: frameColor), + _buildCameraCorner(bottomLeft: true, color: frameColor), + _buildCameraCorner(bottomRight: true, color: frameColor), Center( child: Container( width: 24, @@ -824,32 +826,32 @@ class _CaptureScreenState extends State // Coins du cadre (inchangés) // ───────────────────────────────────────────────────────────────────────── Widget _buildCameraCorner({ - bool TopLeft = false, - bool TopRight = false, - bool BottomLeft = false, - bool BottomRight = false, + bool topLeft = false, + bool topRight = false, + bool bottomLeft = false, + bool bottomRight = false, Color color = const Color(0xFF00FF00), }) { return Positioned( - top: (TopLeft || TopRight) ? 10 : null, - bottom: (BottomLeft || BottomRight) ? 10 : null, - left: (TopLeft || BottomLeft) ? 10 : null, - right: (TopRight || BottomRight) ? 10 : null, + top: (topLeft || topRight) ? 10 : null, + bottom: (bottomLeft || bottomRight) ? 10 : null, + left: (topLeft || bottomLeft) ? 10 : null, + right: (topRight || bottomRight) ? 10 : null, child: Container( width: 20, height: 20, decoration: BoxDecoration( border: Border( - top: (TopLeft || TopRight) + top: (topLeft || topRight) ? BorderSide(color: color, width: 4) : BorderSide.none, - bottom: (BottomLeft || BottomRight) + bottom: (bottomLeft || bottomRight) ? BorderSide(color: color, width: 4) : BorderSide.none, - left: (TopLeft || BottomLeft) + left: (topLeft || bottomLeft) ? BorderSide(color: color, width: 4) : BorderSide.none, - right: (TopRight || BottomRight) + right: (topRight || bottomRight) ? BorderSide(color: color, width: 4) : BorderSide.none, ), diff --git a/lib/features/crop/crop_screen.dart b/lib/features/crop/crop_screen.dart index 2b4cd69c..5ff0567a 100644 --- a/lib/features/crop/crop_screen.dart +++ b/lib/features/crop/crop_screen.dart @@ -114,7 +114,7 @@ class _CropScreenState extends State { Flexible( child: Text( 'Alignez et pivotez la cible sur la croix', - style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 14, fontWeight: FontWeight.w500), + style: TextStyle(color: Colors.white.withValues(alpha: 0.8), fontSize: 14, fontWeight: FontWeight.w500), ), ), ], @@ -122,7 +122,7 @@ class _CropScreenState extends State { const SizedBox(height: 4), Text( 'Glissez à un doigt pour déplacer, pincez pour zoomer', - style: TextStyle(color: Colors.white.withOpacity(0.5), fontSize: 12), + style: TextStyle(color: Colors.white.withValues(alpha: 0.5), fontSize: 12), ), ], ), @@ -274,7 +274,7 @@ class _CropScreenState extends State { child: Transform( transform: Matrix4.identity() ..setTranslationRaw(_offset.dx, _offset.dy, 0) - ..scale(_scale, _scale) + ..scaleByDouble(_scale, _scale, _scale, 1.0) ..rotateZ(_rotation * (math.pi / 180)), alignment: Alignment.center, child: Image.file( @@ -296,14 +296,14 @@ class _CropScreenState extends State { child: Container( width: double.infinity, height: 1.5, - color: const Color(0xFF00FF00).withOpacity(0.6), + color: const Color(0xFF00FF00).withValues(alpha: 0.6), ), ), Center( child: Container( width: 1.5, height: double.infinity, - color: const Color(0xFF00FF00).withOpacity(0.6), + color: const Color(0xFF00FF00).withValues(alpha: 0.6), ), ), ], diff --git a/lib/features/garage/weapon_detail_screen.dart b/lib/features/garage/weapon_detail_screen.dart index 979e6d4f..64616191 100644 --- a/lib/features/garage/weapon_detail_screen.dart +++ b/lib/features/garage/weapon_detail_screen.dart @@ -52,7 +52,7 @@ class _WeaponDetailScreenState extends State { actions: [ IconButton( icon: const Icon(Icons.edit), - onPressed: () => _showEditWeaponDialog(context), + onPressed: _showEditWeaponDialog, ), ], ), @@ -74,7 +74,7 @@ class _WeaponDetailScreenState extends State { ), ), floatingActionButton: FloatingActionButton.extended( - onPressed: () => _showAddMaintenanceDialog(context), + onPressed: _showAddMaintenanceDialog, label: const Text('Entretien'), icon: const Icon(Icons.build), ), @@ -166,7 +166,7 @@ class _WeaponDetailScreenState extends State { padding: const EdgeInsets.all(8), minimumSize: const Size(36, 36), ), - onPressed: () => _showEditAccessoriesDialog(context), + onPressed: _showEditAccessoriesDialog, ), ], ), @@ -223,7 +223,7 @@ class _WeaponDetailScreenState extends State { title: Text(entry.type.displayName), subtitle: Text(entry.description), trailing: Text(DateFormat('dd/MM/yy').format(entry.date), style: const TextStyle(fontSize: 12)), - onLongPress: () => _confirmDeleteMaintenance(context, entry), + onLongPress: () => _confirmDeleteMaintenance(entry), ), ); }, @@ -279,7 +279,9 @@ class _WeaponDetailScreenState extends State { ); } - void _showEditWeaponDialog(BuildContext context) async { + void _showEditWeaponDialog() async { + // Capturé AVANT l'await : plus aucun usage du context après le dialogue. + final repository = context.read(); final nameController = TextEditingController(text: _weapon.name); final caliberController = TextEditingController(text: _weapon.caliber); final magCountController = TextEditingController(text: _weapon.magazineCount.toString()); @@ -309,7 +311,7 @@ class _WeaponDetailScreenState extends State { decoration: const InputDecoration(labelText: 'Surnom / Custom Name', hintText: 'ex: Mon Glock de Compète'), )), DropdownButtonFormField( - value: selectedType, + initialValue: selectedType, decoration: const InputDecoration(labelText: 'Type'), items: WeaponType.values.map((t) => DropdownMenuItem(value: t, child: Text(t.displayName))).toList(), onChanged: (v) => setState(() => selectedType = v!), @@ -382,9 +384,9 @@ class _WeaponDetailScreenState extends State { trigger: triggerController.text.isEmpty ? null : triggerController.text, customName: customNameController.text.isEmpty ? null : customNameController.text, ); - - final repository = context.read(); + await repository.updateWeapon(updatedWeapon); + if (!mounted) return; setState(() { _weapon = updatedWeapon; }); @@ -395,7 +397,9 @@ class _WeaponDetailScreenState extends State { // fortement des accessoires, on ne modifie PAS l'arme existante : on crée une // nouvelle arme (même modèle, accessoires différents) dans l'armurerie afin de // pouvoir comparer les scores selon la configuration utilisée. - void _showEditAccessoriesDialog(BuildContext context) async { + void _showEditAccessoriesDialog() async { + // Capturé AVANT l'await : plus aucun usage du context après le dialogue. + final repository = context.read(); final opticController = TextEditingController(text: _weapon.optic); final silencerController = TextEditingController(text: _weapon.silencer); final triggerController = TextEditingController(text: _weapon.trigger); @@ -444,7 +448,7 @@ class _WeaponDetailScreenState extends State { ), ); - if (result != true) return; + if (result != true || !mounted) return; final newOptic = opticController.text.isEmpty ? null : opticController.text; final newSilencer = silencerController.text.isEmpty ? null : silencerController.text; @@ -456,11 +460,9 @@ class _WeaponDetailScreenState extends State { newSilencer == _weapon.silencer && newTrigger == _weapon.trigger; if (unchanged) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Aucun accessoire modifié, aucune configuration créée.')), - ); - } + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Aucun accessoire modifié, aucune configuration créée.')), + ); return; } @@ -472,7 +474,6 @@ class _WeaponDetailScreenState extends State { newCustomName = accessories.isEmpty ? null : '${_weapon.name} ($accessories)'; } - final repository = context.read(); final newWeapon = await repository.addWeapon( name: _weapon.name, type: _weapon.type, @@ -497,7 +498,9 @@ class _WeaponDetailScreenState extends State { ); } - void _showAddMaintenanceDialog(BuildContext context) async { + void _showAddMaintenanceDialog() async { + // Capturé AVANT l'await : plus aucun usage du context après le dialogue. + final repository = context.read(); final descController = TextEditingController(); MaintenanceType selectedType = MaintenanceType.cleaning; DateTime selectedDate = DateTime.now(); @@ -511,7 +514,7 @@ class _WeaponDetailScreenState extends State { mainAxisSize: MainAxisSize.min, children: [ DropdownButtonFormField( - value: selectedType, + initialValue: selectedType, decoration: const InputDecoration(labelText: 'Type d\'intervention'), items: MaintenanceType.values.map((t) => DropdownMenuItem(value: t, child: Text(t.displayName))).toList(), onChanged: (v) => setState(() => selectedType = v!), @@ -553,7 +556,6 @@ class _WeaponDetailScreenState extends State { ); if (result == true && descController.text.isNotEmpty) { - final repository = context.read(); await repository.addMaintenanceEntry( weaponId: _weapon.id, type: selectedType, @@ -561,11 +563,14 @@ class _WeaponDetailScreenState extends State { roundsSinceLast: _totalRounds, date: selectedDate, ); + if (!mounted) return; _loadData(); } } - void _confirmDeleteMaintenance(BuildContext context, MaintenanceEntry entry) async { + void _confirmDeleteMaintenance(MaintenanceEntry entry) async { + // Capturé AVANT l'await : plus aucun usage du context après le dialogue. + final repository = context.read(); final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( @@ -582,8 +587,8 @@ class _WeaponDetailScreenState extends State { ); if (confirmed == true) { - final repository = context.read(); await repository.deleteMaintenanceEntry(entry.id); + if (!mounted) return; _loadData(); } } diff --git a/lib/features/garage/weapon_list_screen.dart b/lib/features/garage/weapon_list_screen.dart index d697071d..b119d4ae 100644 --- a/lib/features/garage/weapon_list_screen.dart +++ b/lib/features/garage/weapon_list_screen.dart @@ -46,7 +46,7 @@ class _WeaponListScreenState extends State { ? _buildEmptyState() : _buildWeaponList(), floatingActionButton: FloatingActionButton( - onPressed: () => _showAddWeaponDialog(context), + onPressed: _showAddWeaponDialog, child: const Icon(Icons.add), ), ); @@ -62,7 +62,7 @@ class _WeaponListScreenState extends State { const Text('Aucune arme enregistrée'), const SizedBox(height: 24), ElevatedButton( - onPressed: () => _showAddWeaponDialog(context), + onPressed: _showAddWeaponDialog, child: const Text('Ajouter ma première arme'), ), ], @@ -88,7 +88,7 @@ class _WeaponListScreenState extends State { ); _loadWeapons(); // Reload in case it was edited or maintenance was added }, - onLongPress: () => _confirmDelete(context, weapon), + onLongPress: () => _confirmDelete(weapon), child: Padding( // La hauteur du cadre s'adapte automatiquement à la liste d'accessoires. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), @@ -179,7 +179,9 @@ class _WeaponListScreenState extends State { }).toList(); } - void _showAddWeaponDialog(BuildContext context) async { + void _showAddWeaponDialog() async { + // Capturé AVANT l'await : plus aucun usage du context après le dialogue. + final repository = context.read(); final nameController = TextEditingController(); final caliberController = TextEditingController(); final magCountController = TextEditingController(text: '2'); @@ -200,7 +202,7 @@ class _WeaponListScreenState extends State { decoration: const InputDecoration(labelText: 'Nom de l\'arme', hintText: 'ex: Glock 17'), ), DropdownButtonFormField( - value: selectedType, + initialValue: selectedType, decoration: const InputDecoration(labelText: 'Type'), items: WeaponType.values.map((t) => DropdownMenuItem(value: t, child: Text(t.displayName))).toList(), onChanged: (v) => setState(() => selectedType = v!), @@ -240,7 +242,6 @@ class _WeaponListScreenState extends State { ); if (result == true && nameController.text.isNotEmpty) { - final repository = context.read(); await repository.addWeapon( name: nameController.text, type: selectedType, @@ -248,11 +249,14 @@ class _WeaponListScreenState extends State { magazineCount: int.tryParse(magCountController.text) ?? 1, magazineCapacity: int.tryParse(magCapController.text) ?? 10, ); + if (!mounted) return; _loadWeapons(); } } - void _confirmDelete(BuildContext context, Weapon weapon) async { + void _confirmDelete(Weapon weapon) async { + // Capturé AVANT l'await : plus aucun usage du context après le dialogue. + final repository = context.read(); final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( @@ -269,8 +273,8 @@ class _WeaponListScreenState extends State { ); if (confirmed == true) { - final repository = context.read(); await repository.deleteWeapon(weapon.id); + if (!mounted) return; _loadWeapons(); } } diff --git a/lib/features/history/history_screen.dart b/lib/features/history/history_screen.dart index 9813f398..0bc0fbe5 100644 --- a/lib/features/history/history_screen.dart +++ b/lib/features/history/history_screen.dart @@ -111,7 +111,7 @@ class _HistoryScreenState extends State { onSurface: Colors.black87, // Texte des dates (Noir sur Blanc) secondary: AppTheme.primaryColor, ), - dialogBackgroundColor: Colors.white, + dialogTheme: const DialogThemeData(backgroundColor: Colors.white), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom(foregroundColor: AppTheme.primaryColor), ), diff --git a/lib/features/session/session_setup_screen.dart b/lib/features/session/session_setup_screen.dart index 733705ec..434b5bc1 100644 --- a/lib/features/session/session_setup_screen.dart +++ b/lib/features/session/session_setup_screen.dart @@ -168,7 +168,7 @@ class _SessionSetupScreenState extends State { Column( children: [ DropdownButtonFormField( - value: _selectedWeapon, + initialValue: _selectedWeapon, decoration: InputDecoration( labelText: 'Sélectionner une arme', prefixIcon: const Icon(Icons.shield), diff --git a/lib/features/settings/settings_screen.dart b/lib/features/settings/settings_screen.dart index b203a4c7..ae9471e0 100644 --- a/lib/features/settings/settings_screen.dart +++ b/lib/features/settings/settings_screen.dart @@ -67,7 +67,7 @@ class _SettingsScreenState extends State { if (mounted) setState(() => _isLoadingStats = false); } } catch (e) { - print('Erreur lors du chargement des statistiques: $e'); + debugPrint('Erreur lors du chargement des statistiques: $e'); if (mounted) setState(() => _isLoadingStats = false); } } @@ -374,7 +374,7 @@ class _SettingsScreenState extends State { subtitle: const Text('Aidez-nous à améliorer la détection tout en gagnant des avantages', style: TextStyle(fontSize: 12)), secondary: const Icon(Icons.psychology, color: AppTheme.textPrimary), value: _isUploadEnabled, - activeColor: AppTheme.primaryColor, + activeThumbColor: AppTheme.primaryColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.0), side: BorderSide(color: Colors.grey.withAlpha(50), width: 1), diff --git a/lib/features/statistics/statistics_screen.dart b/lib/features/statistics/statistics_screen.dart index c98c3514..eb5c5d3e 100644 --- a/lib/features/statistics/statistics_screen.dart +++ b/lib/features/statistics/statistics_screen.dart @@ -24,7 +24,7 @@ class StatisticsScreen extends StatefulWidget { class _StatisticsScreenState extends State { final StatisticsService _statisticsService = StatisticsService(); - StatsPeriod _selectedPeriod = StatsPeriod.all; + final StatsPeriod _selectedPeriod = StatsPeriod.all; SessionStatistics? _statistics; bool _isLoading = true; List _allSessions = []; diff --git a/pubspec.yaml b/pubspec.yaml index 2fd58a6a..a4d0b03a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: bully -description: "A new Flutter project." +description: "Application d'analyse de cibles de tir : capture, centrage, plotting manuel des impacts, scores et statistiques." # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev