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:
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
85
lib/features/analysis/widgets/shot_details_sheet.dart
Normal file
85
lib/features/analysis/widgets/shot_details_sheet.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
Reference in New Issue
Block a user