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:
19
CLAUDE.md
19
CLAUDE.md
@@ -41,10 +41,12 @@ flutter test --coverage
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### Analyse de cibles de tir
|
### Capture de cibles de tir
|
||||||
- Support de cibles concentriques (anneaux) et silhouettes
|
- Support de cibles concentriques (anneaux) et silhouettes
|
||||||
- Chargement d'images de cibles depuis la galerie ou la caméra
|
- 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
|
### Calibration des cibles
|
||||||
- Ajustement manuel du centre, du rayon et du nombre d'anneaux (1-10)
|
- 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
|
- Slider global pour redimensionner tous les anneaux proportionnellement
|
||||||
- Visualisation en temps réel des zones de score
|
- Visualisation en temps réel des zones de score
|
||||||
|
|
||||||
### Détection d'impacts
|
### Placement des impacts
|
||||||
- **Ajout manuel** : cliquer sur l'image pour placer un impact
|
- **Éditeur d'impacts plein écran** : tap pour ajouter, tap sur un impact pour
|
||||||
- **Détection automatique** : algorithme de détection de blobs avec paramètres ajustables
|
l'éditer (score/suppression), appui long pour déplacer, pincer pour zoomer
|
||||||
- Seuil de luminosité
|
- Le placement est entièrement manuel ; le bouton ↻ de l'écran de plotting
|
||||||
- Taille min/max des impacts
|
efface tous les impacts sans toucher à la calibration
|
||||||
- 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
|
|
||||||
|
|
||||||
### Calcul des scores
|
### Calcul des scores
|
||||||
- Score automatique basé sur la position de l'impact dans les zones
|
- Score automatique basé sur la position de l'impact dans les zones
|
||||||
|
|||||||
@@ -380,17 +380,71 @@ class DatabaseHelper {
|
|||||||
limit: limit,
|
limit: limit,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
);
|
);
|
||||||
|
if (sessionMaps.isEmpty) return [];
|
||||||
|
|
||||||
final sessions = <Session>[];
|
// 3 requêtes groupées au lieu d'une cascade par session puis par analyse
|
||||||
for (final sessionMap in sessionMaps) {
|
// (N+1) : l'ancien code refaisait un getSession complet pour chaque ligne,
|
||||||
final sessionId = sessionMap['id'] as String;
|
// ce qui ralentissait l'accueil/historique/stats au fil des mois.
|
||||||
final session = await getSession(sessionId);
|
final sessionIds = sessionMaps.map((m) => m['id'] as String).toList();
|
||||||
if (session != null) {
|
final analysisMaps = await _queryIn(
|
||||||
sessions.add(session);
|
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 {
|
Future<int> deleteSession(String id) async {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import 'package:provider/provider.dart';
|
|||||||
import '../../core/constants/app_constants.dart';
|
import '../../core/constants/app_constants.dart';
|
||||||
import '../../core/theme/app_theme.dart';
|
import '../../core/theme/app_theme.dart';
|
||||||
import '../../data/models/target_type.dart';
|
import '../../data/models/target_type.dart';
|
||||||
import '../../data/models/shot.dart';
|
|
||||||
import '../../data/repositories/session_repository.dart';
|
import '../../data/repositories/session_repository.dart';
|
||||||
import '../../services/score_calculator_service.dart';
|
import '../../services/score_calculator_service.dart';
|
||||||
import '../../services/grouping_analyzer_service.dart';
|
import '../../services/grouping_analyzer_service.dart';
|
||||||
@@ -26,6 +25,7 @@ import 'widgets/target_overlay.dart';
|
|||||||
import 'widgets/target_calibration.dart';
|
import 'widgets/target_calibration.dart';
|
||||||
import 'widgets/score_card.dart';
|
import 'widgets/score_card.dart';
|
||||||
import 'widgets/grouping_stats.dart';
|
import 'widgets/grouping_stats.dart';
|
||||||
|
import 'widgets/shot_details_sheet.dart';
|
||||||
|
|
||||||
class AnalysisScreen extends StatelessWidget {
|
class AnalysisScreen extends StatelessWidget {
|
||||||
final String imagePath;
|
final String imagePath;
|
||||||
@@ -364,7 +364,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
widget.cropOffset?.dy ?? 0.0,
|
widget.cropOffset?.dy ?? 0.0,
|
||||||
0.0,
|
0.0,
|
||||||
)
|
)
|
||||||
..scale(1.0, 1.0)
|
|
||||||
..rotateZ(
|
..rotateZ(
|
||||||
(provider.cropRotation) *
|
(provider.cropRotation) *
|
||||||
(math.pi / 180),
|
(math.pi / 180),
|
||||||
@@ -639,7 +638,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
|
|||||||
zoomScale: _currentZoomScale,
|
zoomScale: _currentZoomScale,
|
||||||
// Lecture seule : tap sur impact -> détails (consultation).
|
// Lecture seule : tap sur impact -> détails (consultation).
|
||||||
onShotTapped: (shot) =>
|
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) {
|
void _showSaveSessionDialog(BuildContext context, AnalysisProvider provider) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import 'package:provider/provider.dart';
|
|||||||
import '../../data/models/shot.dart';
|
import '../../data/models/shot.dart';
|
||||||
import 'analysis_provider.dart';
|
import 'analysis_provider.dart';
|
||||||
import 'widgets/target_overlay.dart';
|
import 'widgets/target_overlay.dart';
|
||||||
|
import 'widgets/shot_details_sheet.dart';
|
||||||
|
|
||||||
class ImpactEditorScreen extends StatefulWidget {
|
class ImpactEditorScreen extends StatefulWidget {
|
||||||
const ImpactEditorScreen({super.key});
|
const ImpactEditorScreen({super.key});
|
||||||
@@ -144,7 +145,7 @@ class _ImpactEditorScreenState extends State<ImpactEditorScreen> {
|
|||||||
|
|
||||||
final hit = _hitTestShot(provider, rel);
|
final hit = _hitTestShot(provider, rel);
|
||||||
if (hit != null) {
|
if (hit != null) {
|
||||||
_showShotDetails(context, provider, hit);
|
showShotDetailsSheet(context, provider, hit);
|
||||||
} else {
|
} else {
|
||||||
provider.addShot(rel.dx, rel.dy);
|
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
|
// L'ajout et la sélection sont gérés par le
|
||||||
// GestureDetector parent ci-dessus.
|
// GestureDetector parent ci-dessus.
|
||||||
onShotTapped: (shot) =>
|
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();
|
final now = DateTime.now();
|
||||||
// Cadence ~1 s : assez réactif pour suivre la cible sans saturer le CPU.
|
// Cadence ~1 s : assez réactif pour suivre la cible sans saturer le CPU.
|
||||||
if (lastAnalysis != null &&
|
if (lastAnalysis != null &&
|
||||||
now.difference(lastAnalysis!).inMilliseconds < 1000) return;
|
now.difference(lastAnalysis!).inMilliseconds < 1000) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
lastAnalysis = now;
|
lastAnalysis = now;
|
||||||
_isAnalyzingFrame = true;
|
_isAnalyzingFrame = true;
|
||||||
|
|
||||||
@@ -514,10 +516,10 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
height: MediaQuery.of(context).size.width * 0.85,
|
height: MediaQuery.of(context).size.width * 0.85,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
_buildCameraCorner(TopLeft: true, color: frameColor),
|
_buildCameraCorner(topLeft: true, color: frameColor),
|
||||||
_buildCameraCorner(TopRight: true, color: frameColor),
|
_buildCameraCorner(topRight: true, color: frameColor),
|
||||||
_buildCameraCorner(BottomLeft: true, color: frameColor),
|
_buildCameraCorner(bottomLeft: true, color: frameColor),
|
||||||
_buildCameraCorner(BottomRight: true, color: frameColor),
|
_buildCameraCorner(bottomRight: true, color: frameColor),
|
||||||
Center(
|
Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 24,
|
width: 24,
|
||||||
@@ -824,32 +826,32 @@ class _CaptureScreenState extends State<CaptureScreen>
|
|||||||
// Coins du cadre (inchangés)
|
// Coins du cadre (inchangés)
|
||||||
// ─────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────
|
||||||
Widget _buildCameraCorner({
|
Widget _buildCameraCorner({
|
||||||
bool TopLeft = false,
|
bool topLeft = false,
|
||||||
bool TopRight = false,
|
bool topRight = false,
|
||||||
bool BottomLeft = false,
|
bool bottomLeft = false,
|
||||||
bool BottomRight = false,
|
bool bottomRight = false,
|
||||||
Color color = const Color(0xFF00FF00),
|
Color color = const Color(0xFF00FF00),
|
||||||
}) {
|
}) {
|
||||||
return Positioned(
|
return Positioned(
|
||||||
top: (TopLeft || TopRight) ? 10 : null,
|
top: (topLeft || topRight) ? 10 : null,
|
||||||
bottom: (BottomLeft || BottomRight) ? 10 : null,
|
bottom: (bottomLeft || bottomRight) ? 10 : null,
|
||||||
left: (TopLeft || BottomLeft) ? 10 : null,
|
left: (topLeft || bottomLeft) ? 10 : null,
|
||||||
right: (TopRight || BottomRight) ? 10 : null,
|
right: (topRight || bottomRight) ? 10 : null,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 20,
|
width: 20,
|
||||||
height: 20,
|
height: 20,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
top: (TopLeft || TopRight)
|
top: (topLeft || topRight)
|
||||||
? BorderSide(color: color, width: 4)
|
? BorderSide(color: color, width: 4)
|
||||||
: BorderSide.none,
|
: BorderSide.none,
|
||||||
bottom: (BottomLeft || BottomRight)
|
bottom: (bottomLeft || bottomRight)
|
||||||
? BorderSide(color: color, width: 4)
|
? BorderSide(color: color, width: 4)
|
||||||
: BorderSide.none,
|
: BorderSide.none,
|
||||||
left: (TopLeft || BottomLeft)
|
left: (topLeft || bottomLeft)
|
||||||
? BorderSide(color: color, width: 4)
|
? BorderSide(color: color, width: 4)
|
||||||
: BorderSide.none,
|
: BorderSide.none,
|
||||||
right: (TopRight || BottomRight)
|
right: (topRight || bottomRight)
|
||||||
? BorderSide(color: color, width: 4)
|
? BorderSide(color: color, width: 4)
|
||||||
: BorderSide.none,
|
: BorderSide.none,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ class _CropScreenState extends State<CropScreen> {
|
|||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Alignez et pivotez la cible sur la croix',
|
'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),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'Glissez à un doigt pour déplacer, pincez pour zoomer',
|
'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(
|
child: Transform(
|
||||||
transform: Matrix4.identity()
|
transform: Matrix4.identity()
|
||||||
..setTranslationRaw(_offset.dx, _offset.dy, 0)
|
..setTranslationRaw(_offset.dx, _offset.dy, 0)
|
||||||
..scale(_scale, _scale)
|
..scaleByDouble(_scale, _scale, _scale, 1.0)
|
||||||
..rotateZ(_rotation * (math.pi / 180)),
|
..rotateZ(_rotation * (math.pi / 180)),
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Image.file(
|
child: Image.file(
|
||||||
@@ -296,14 +296,14 @@ class _CropScreenState extends State<CropScreen> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
color: const Color(0xFF00FF00).withOpacity(0.6),
|
color: const Color(0xFF00FF00).withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Center(
|
Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 1.5,
|
width: 1.5,
|
||||||
height: double.infinity,
|
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: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.edit),
|
icon: const Icon(Icons.edit),
|
||||||
onPressed: () => _showEditWeaponDialog(context),
|
onPressed: _showEditWeaponDialog,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -74,7 +74,7 @@ class _WeaponDetailScreenState extends State<WeaponDetailScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
onPressed: () => _showAddMaintenanceDialog(context),
|
onPressed: _showAddMaintenanceDialog,
|
||||||
label: const Text('Entretien'),
|
label: const Text('Entretien'),
|
||||||
icon: const Icon(Icons.build),
|
icon: const Icon(Icons.build),
|
||||||
),
|
),
|
||||||
@@ -166,7 +166,7 @@ class _WeaponDetailScreenState extends State<WeaponDetailScreen> {
|
|||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
minimumSize: const Size(36, 36),
|
minimumSize: const Size(36, 36),
|
||||||
),
|
),
|
||||||
onPressed: () => _showEditAccessoriesDialog(context),
|
onPressed: _showEditAccessoriesDialog,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -223,7 +223,7 @@ class _WeaponDetailScreenState extends State<WeaponDetailScreen> {
|
|||||||
title: Text(entry.type.displayName),
|
title: Text(entry.type.displayName),
|
||||||
subtitle: Text(entry.description),
|
subtitle: Text(entry.description),
|
||||||
trailing: Text(DateFormat('dd/MM/yy').format(entry.date), style: const TextStyle(fontSize: 12)),
|
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 nameController = TextEditingController(text: _weapon.name);
|
||||||
final caliberController = TextEditingController(text: _weapon.caliber);
|
final caliberController = TextEditingController(text: _weapon.caliber);
|
||||||
final magCountController = TextEditingController(text: _weapon.magazineCount.toString());
|
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'),
|
decoration: const InputDecoration(labelText: 'Surnom / Custom Name', hintText: 'ex: Mon Glock de Compète'),
|
||||||
)),
|
)),
|
||||||
DropdownButtonFormField<WeaponType>(
|
DropdownButtonFormField<WeaponType>(
|
||||||
value: selectedType,
|
initialValue: selectedType,
|
||||||
decoration: const InputDecoration(labelText: 'Type'),
|
decoration: const InputDecoration(labelText: 'Type'),
|
||||||
items: WeaponType.values.map((t) => DropdownMenuItem(value: t, child: Text(t.displayName))).toList(),
|
items: WeaponType.values.map((t) => DropdownMenuItem(value: t, child: Text(t.displayName))).toList(),
|
||||||
onChanged: (v) => setState(() => selectedType = v!),
|
onChanged: (v) => setState(() => selectedType = v!),
|
||||||
@@ -383,8 +385,8 @@ class _WeaponDetailScreenState extends State<WeaponDetailScreen> {
|
|||||||
customName: customNameController.text.isEmpty ? null : customNameController.text,
|
customName: customNameController.text.isEmpty ? null : customNameController.text,
|
||||||
);
|
);
|
||||||
|
|
||||||
final repository = context.read<SessionRepository>();
|
|
||||||
await repository.updateWeapon(updatedWeapon);
|
await repository.updateWeapon(updatedWeapon);
|
||||||
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_weapon = updatedWeapon;
|
_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
|
// 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
|
// nouvelle arme (même modèle, accessoires différents) dans l'armurerie afin de
|
||||||
// pouvoir comparer les scores selon la configuration utilisée.
|
// 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 opticController = TextEditingController(text: _weapon.optic);
|
||||||
final silencerController = TextEditingController(text: _weapon.silencer);
|
final silencerController = TextEditingController(text: _weapon.silencer);
|
||||||
final triggerController = TextEditingController(text: _weapon.trigger);
|
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 newOptic = opticController.text.isEmpty ? null : opticController.text;
|
||||||
final newSilencer = silencerController.text.isEmpty ? null : silencerController.text;
|
final newSilencer = silencerController.text.isEmpty ? null : silencerController.text;
|
||||||
@@ -456,11 +460,9 @@ class _WeaponDetailScreenState extends State<WeaponDetailScreen> {
|
|||||||
newSilencer == _weapon.silencer &&
|
newSilencer == _weapon.silencer &&
|
||||||
newTrigger == _weapon.trigger;
|
newTrigger == _weapon.trigger;
|
||||||
if (unchanged) {
|
if (unchanged) {
|
||||||
if (mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Aucun accessoire modifié, aucune configuration créée.')),
|
const SnackBar(content: Text('Aucun accessoire modifié, aucune configuration créée.')),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,7 +474,6 @@ class _WeaponDetailScreenState extends State<WeaponDetailScreen> {
|
|||||||
newCustomName = accessories.isEmpty ? null : '${_weapon.name} ($accessories)';
|
newCustomName = accessories.isEmpty ? null : '${_weapon.name} ($accessories)';
|
||||||
}
|
}
|
||||||
|
|
||||||
final repository = context.read<SessionRepository>();
|
|
||||||
final newWeapon = await repository.addWeapon(
|
final newWeapon = await repository.addWeapon(
|
||||||
name: _weapon.name,
|
name: _weapon.name,
|
||||||
type: _weapon.type,
|
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();
|
final descController = TextEditingController();
|
||||||
MaintenanceType selectedType = MaintenanceType.cleaning;
|
MaintenanceType selectedType = MaintenanceType.cleaning;
|
||||||
DateTime selectedDate = DateTime.now();
|
DateTime selectedDate = DateTime.now();
|
||||||
@@ -511,7 +514,7 @@ class _WeaponDetailScreenState extends State<WeaponDetailScreen> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
DropdownButtonFormField<MaintenanceType>(
|
DropdownButtonFormField<MaintenanceType>(
|
||||||
value: selectedType,
|
initialValue: selectedType,
|
||||||
decoration: const InputDecoration(labelText: 'Type d\'intervention'),
|
decoration: const InputDecoration(labelText: 'Type d\'intervention'),
|
||||||
items: MaintenanceType.values.map((t) => DropdownMenuItem(value: t, child: Text(t.displayName))).toList(),
|
items: MaintenanceType.values.map((t) => DropdownMenuItem(value: t, child: Text(t.displayName))).toList(),
|
||||||
onChanged: (v) => setState(() => selectedType = v!),
|
onChanged: (v) => setState(() => selectedType = v!),
|
||||||
@@ -553,7 +556,6 @@ class _WeaponDetailScreenState extends State<WeaponDetailScreen> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (result == true && descController.text.isNotEmpty) {
|
if (result == true && descController.text.isNotEmpty) {
|
||||||
final repository = context.read<SessionRepository>();
|
|
||||||
await repository.addMaintenanceEntry(
|
await repository.addMaintenanceEntry(
|
||||||
weaponId: _weapon.id,
|
weaponId: _weapon.id,
|
||||||
type: selectedType,
|
type: selectedType,
|
||||||
@@ -561,11 +563,14 @@ class _WeaponDetailScreenState extends State<WeaponDetailScreen> {
|
|||||||
roundsSinceLast: _totalRounds,
|
roundsSinceLast: _totalRounds,
|
||||||
date: selectedDate,
|
date: selectedDate,
|
||||||
);
|
);
|
||||||
|
if (!mounted) return;
|
||||||
_loadData();
|
_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>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
@@ -582,8 +587,8 @@ class _WeaponDetailScreenState extends State<WeaponDetailScreen> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (confirmed == true) {
|
if (confirmed == true) {
|
||||||
final repository = context.read<SessionRepository>();
|
|
||||||
await repository.deleteMaintenanceEntry(entry.id);
|
await repository.deleteMaintenanceEntry(entry.id);
|
||||||
|
if (!mounted) return;
|
||||||
_loadData();
|
_loadData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class _WeaponListScreenState extends State<WeaponListScreen> {
|
|||||||
? _buildEmptyState()
|
? _buildEmptyState()
|
||||||
: _buildWeaponList(),
|
: _buildWeaponList(),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
onPressed: () => _showAddWeaponDialog(context),
|
onPressed: _showAddWeaponDialog,
|
||||||
child: const Icon(Icons.add),
|
child: const Icon(Icons.add),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -62,7 +62,7 @@ class _WeaponListScreenState extends State<WeaponListScreen> {
|
|||||||
const Text('Aucune arme enregistrée'),
|
const Text('Aucune arme enregistrée'),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => _showAddWeaponDialog(context),
|
onPressed: _showAddWeaponDialog,
|
||||||
child: const Text('Ajouter ma première arme'),
|
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
|
_loadWeapons(); // Reload in case it was edited or maintenance was added
|
||||||
},
|
},
|
||||||
onLongPress: () => _confirmDelete(context, weapon),
|
onLongPress: () => _confirmDelete(weapon),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
// La hauteur du cadre s'adapte automatiquement à la liste d'accessoires.
|
// La hauteur du cadre s'adapte automatiquement à la liste d'accessoires.
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
@@ -179,7 +179,9 @@ class _WeaponListScreenState extends State<WeaponListScreen> {
|
|||||||
}).toList();
|
}).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 nameController = TextEditingController();
|
||||||
final caliberController = TextEditingController();
|
final caliberController = TextEditingController();
|
||||||
final magCountController = TextEditingController(text: '2');
|
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'),
|
decoration: const InputDecoration(labelText: 'Nom de l\'arme', hintText: 'ex: Glock 17'),
|
||||||
),
|
),
|
||||||
DropdownButtonFormField<WeaponType>(
|
DropdownButtonFormField<WeaponType>(
|
||||||
value: selectedType,
|
initialValue: selectedType,
|
||||||
decoration: const InputDecoration(labelText: 'Type'),
|
decoration: const InputDecoration(labelText: 'Type'),
|
||||||
items: WeaponType.values.map((t) => DropdownMenuItem(value: t, child: Text(t.displayName))).toList(),
|
items: WeaponType.values.map((t) => DropdownMenuItem(value: t, child: Text(t.displayName))).toList(),
|
||||||
onChanged: (v) => setState(() => selectedType = v!),
|
onChanged: (v) => setState(() => selectedType = v!),
|
||||||
@@ -240,7 +242,6 @@ class _WeaponListScreenState extends State<WeaponListScreen> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (result == true && nameController.text.isNotEmpty) {
|
if (result == true && nameController.text.isNotEmpty) {
|
||||||
final repository = context.read<SessionRepository>();
|
|
||||||
await repository.addWeapon(
|
await repository.addWeapon(
|
||||||
name: nameController.text,
|
name: nameController.text,
|
||||||
type: selectedType,
|
type: selectedType,
|
||||||
@@ -248,11 +249,14 @@ class _WeaponListScreenState extends State<WeaponListScreen> {
|
|||||||
magazineCount: int.tryParse(magCountController.text) ?? 1,
|
magazineCount: int.tryParse(magCountController.text) ?? 1,
|
||||||
magazineCapacity: int.tryParse(magCapController.text) ?? 10,
|
magazineCapacity: int.tryParse(magCapController.text) ?? 10,
|
||||||
);
|
);
|
||||||
|
if (!mounted) return;
|
||||||
_loadWeapons();
|
_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>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
@@ -269,8 +273,8 @@ class _WeaponListScreenState extends State<WeaponListScreen> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (confirmed == true) {
|
if (confirmed == true) {
|
||||||
final repository = context.read<SessionRepository>();
|
|
||||||
await repository.deleteWeapon(weapon.id);
|
await repository.deleteWeapon(weapon.id);
|
||||||
|
if (!mounted) return;
|
||||||
_loadWeapons();
|
_loadWeapons();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
|
|||||||
onSurface: Colors.black87, // Texte des dates (Noir sur Blanc)
|
onSurface: Colors.black87, // Texte des dates (Noir sur Blanc)
|
||||||
secondary: AppTheme.primaryColor,
|
secondary: AppTheme.primaryColor,
|
||||||
),
|
),
|
||||||
dialogBackgroundColor: Colors.white,
|
dialogTheme: const DialogThemeData(backgroundColor: Colors.white),
|
||||||
textButtonTheme: TextButtonThemeData(
|
textButtonTheme: TextButtonThemeData(
|
||||||
style: TextButton.styleFrom(foregroundColor: AppTheme.primaryColor),
|
style: TextButton.styleFrom(foregroundColor: AppTheme.primaryColor),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ class _SessionSetupScreenState extends State<SessionSetupScreen> {
|
|||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
DropdownButtonFormField<Weapon>(
|
DropdownButtonFormField<Weapon>(
|
||||||
value: _selectedWeapon,
|
initialValue: _selectedWeapon,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Sélectionner une arme',
|
labelText: 'Sélectionner une arme',
|
||||||
prefixIcon: const Icon(Icons.shield),
|
prefixIcon: const Icon(Icons.shield),
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
if (mounted) setState(() => _isLoadingStats = false);
|
if (mounted) setState(() => _isLoadingStats = false);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Erreur lors du chargement des statistiques: $e');
|
debugPrint('Erreur lors du chargement des statistiques: $e');
|
||||||
if (mounted) setState(() => _isLoadingStats = false);
|
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)),
|
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),
|
secondary: const Icon(Icons.psychology, color: AppTheme.textPrimary),
|
||||||
value: _isUploadEnabled,
|
value: _isUploadEnabled,
|
||||||
activeColor: AppTheme.primaryColor,
|
activeThumbColor: AppTheme.primaryColor,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12.0),
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
side: BorderSide(color: Colors.grey.withAlpha(50), width: 1),
|
side: BorderSide(color: Colors.grey.withAlpha(50), width: 1),
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class StatisticsScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _StatisticsScreenState extends State<StatisticsScreen> {
|
class _StatisticsScreenState extends State<StatisticsScreen> {
|
||||||
final StatisticsService _statisticsService = StatisticsService();
|
final StatisticsService _statisticsService = StatisticsService();
|
||||||
StatsPeriod _selectedPeriod = StatsPeriod.all;
|
final StatsPeriod _selectedPeriod = StatsPeriod.all;
|
||||||
SessionStatistics? _statistics;
|
SessionStatistics? _statistics;
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
List<Session> _allSessions = [];
|
List<Session> _allSessions = [];
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: bully
|
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
|
# The following line prevents the package from being accidentally published to
|
||||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
# 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
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
|
|||||||
Reference in New Issue
Block a user