refactor: perf getAllSessions, zéro warning analyze, docs à jour

- 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 <noreply@anthropic.com>
This commit is contained in:
qguillaume
2026-07-04 10:40:25 +02:00
parent 8d5641c0a3
commit cec9838e9b
14 changed files with 229 additions and 229 deletions

View File

@@ -380,17 +380,71 @@ class DatabaseHelper {
limit: limit,
offset: offset,
);
if (sessionMaps.isEmpty) return [];
final sessions = <Session>[];
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 = <String, List<Shot>>{};
for (final map in shotMaps) {
(shotsByAnalysis[map['analysis_id'] as String] ??= [])
.add(Shot.fromMap(map));
}
return sessions;
final analysesBySession = <String, List<TargetAnalysis>>{};
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<List<Map<String, Object?>>> _queryIn(
Database db,
String table,
String column,
List<String> values,
) async {
if (values.isEmpty) return [];
const chunkSize = 500;
final results = <Map<String, Object?>>[];
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<int> deleteSession(String id) async {

View File

@@ -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<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,

View File

@@ -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<ImpactEditorScreen> {
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<ImpactEditorScreen> {
// 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<ImpactEditorScreen> {
),
);
}
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),
),
),
),
],
),
],
),
),
);
}
}

View File

@@ -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<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),
),
),
),
],
),
],
),
),
);
}

View File

@@ -305,7 +305,9 @@ class _CaptureScreenState extends State<CaptureScreen>
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<CaptureScreen>
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<CaptureScreen>
// 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,
),

View File

@@ -114,7 +114,7 @@ class _CropScreenState extends State<CropScreen> {
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<CropScreen> {
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<CropScreen> {
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<CropScreen> {
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),
),
),
],

View File

@@ -52,7 +52,7 @@ class _WeaponDetailScreenState extends State<WeaponDetailScreen> {
actions: [
IconButton(
icon: const Icon(Icons.edit),
onPressed: () => _showEditWeaponDialog(context),
onPressed: _showEditWeaponDialog,
),
],
),
@@ -74,7 +74,7 @@ class _WeaponDetailScreenState extends State<WeaponDetailScreen> {
),
),
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<WeaponDetailScreen> {
padding: const EdgeInsets.all(8),
minimumSize: const Size(36, 36),
),
onPressed: () => _showEditAccessoriesDialog(context),
onPressed: _showEditAccessoriesDialog,
),
],
),
@@ -223,7 +223,7 @@ class _WeaponDetailScreenState extends State<WeaponDetailScreen> {
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<WeaponDetailScreen> {
);
}
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<SessionRepository>();
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<WeaponDetailScreen> {
decoration: const InputDecoration(labelText: 'Surnom / Custom Name', hintText: 'ex: Mon Glock de Compète'),
)),
DropdownButtonFormField<WeaponType>(
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<WeaponDetailScreen> {
trigger: triggerController.text.isEmpty ? null : triggerController.text,
customName: customNameController.text.isEmpty ? null : customNameController.text,
);
final repository = context.read<SessionRepository>();
await repository.updateWeapon(updatedWeapon);
if (!mounted) return;
setState(() {
_weapon = updatedWeapon;
});
@@ -395,7 +397,9 @@ class _WeaponDetailScreenState extends State<WeaponDetailScreen> {
// 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<SessionRepository>();
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<WeaponDetailScreen> {
),
);
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<WeaponDetailScreen> {
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<WeaponDetailScreen> {
newCustomName = accessories.isEmpty ? null : '${_weapon.name} ($accessories)';
}
final repository = context.read<SessionRepository>();
final newWeapon = await repository.addWeapon(
name: _weapon.name,
type: _weapon.type,
@@ -497,7 +498,9 @@ class _WeaponDetailScreenState extends State<WeaponDetailScreen> {
);
}
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<SessionRepository>();
final descController = TextEditingController();
MaintenanceType selectedType = MaintenanceType.cleaning;
DateTime selectedDate = DateTime.now();
@@ -511,7 +514,7 @@ class _WeaponDetailScreenState extends State<WeaponDetailScreen> {
mainAxisSize: MainAxisSize.min,
children: [
DropdownButtonFormField<MaintenanceType>(
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<WeaponDetailScreen> {
);
if (result == true && descController.text.isNotEmpty) {
final repository = context.read<SessionRepository>();
await repository.addMaintenanceEntry(
weaponId: _weapon.id,
type: selectedType,
@@ -561,11 +563,14 @@ class _WeaponDetailScreenState extends State<WeaponDetailScreen> {
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<SessionRepository>();
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
@@ -582,8 +587,8 @@ class _WeaponDetailScreenState extends State<WeaponDetailScreen> {
);
if (confirmed == true) {
final repository = context.read<SessionRepository>();
await repository.deleteMaintenanceEntry(entry.id);
if (!mounted) return;
_loadData();
}
}

View File

@@ -46,7 +46,7 @@ class _WeaponListScreenState extends State<WeaponListScreen> {
? _buildEmptyState()
: _buildWeaponList(),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddWeaponDialog(context),
onPressed: _showAddWeaponDialog,
child: const Icon(Icons.add),
),
);
@@ -62,7 +62,7 @@ class _WeaponListScreenState extends State<WeaponListScreen> {
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<WeaponListScreen> {
);
_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<WeaponListScreen> {
}).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<SessionRepository>();
final nameController = TextEditingController();
final caliberController = TextEditingController();
final magCountController = TextEditingController(text: '2');
@@ -200,7 +202,7 @@ class _WeaponListScreenState extends State<WeaponListScreen> {
decoration: const InputDecoration(labelText: 'Nom de l\'arme', hintText: 'ex: Glock 17'),
),
DropdownButtonFormField<WeaponType>(
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<WeaponListScreen> {
);
if (result == true && nameController.text.isNotEmpty) {
final repository = context.read<SessionRepository>();
await repository.addWeapon(
name: nameController.text,
type: selectedType,
@@ -248,11 +249,14 @@ class _WeaponListScreenState extends State<WeaponListScreen> {
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<SessionRepository>();
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
@@ -269,8 +273,8 @@ class _WeaponListScreenState extends State<WeaponListScreen> {
);
if (confirmed == true) {
final repository = context.read<SessionRepository>();
await repository.deleteWeapon(weapon.id);
if (!mounted) return;
_loadWeapons();
}
}

View File

@@ -111,7 +111,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
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),
),

View File

@@ -168,7 +168,7 @@ class _SessionSetupScreenState extends State<SessionSetupScreen> {
Column(
children: [
DropdownButtonFormField<Weapon>(
value: _selectedWeapon,
initialValue: _selectedWeapon,
decoration: InputDecoration(
labelText: 'Sélectionner une arme',
prefixIcon: const Icon(Icons.shield),

View File

@@ -67,7 +67,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
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<SettingsScreen> {
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),

View File

@@ -24,7 +24,7 @@ class StatisticsScreen extends StatefulWidget {
class _StatisticsScreenState extends State<StatisticsScreen> {
final StatisticsService _statisticsService = StatisticsService();
StatsPeriod _selectedPeriod = StatsPeriod.all;
final StatsPeriod _selectedPeriod = StatsPeriod.all;
SessionStatistics? _statistics;
bool _isLoading = true;
List<Session> _allSessions = [];