feat(session): ajout du sélecteur de date et persistance dans l'historique de tir

This commit is contained in:
qguillaume
2026-05-27 00:57:26 +02:00
parent e570bd4296
commit 78c9fc96c9
6 changed files with 111 additions and 54 deletions

View File

@@ -1,3 +1,4 @@
import 'dart:io';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import '../models/session.dart';
@@ -285,9 +286,14 @@ class DatabaseHelper {
Future<int> insertSession(Session session) async {
final db = await database;
return await db.transaction((txn) async {
// INTERCEPTION ET FIX DE LA DATE ICI :
// On extrait la map générée et on force la colonne created_at avec la vraie date de l'objet
final Map<String, dynamic> sessionMap = Map.from(session.toMap());
sessionMap['created_at'] = session.createdAt.toIso8601String();
await txn.insert(
AppConstants.sessionsTable,
session.toMap(),
sessionMap, // On utilise notre map corrigée au lieu de session.toMap() direct !
conflictAlgorithm: ConflictAlgorithm.replace,
);

View File

@@ -27,6 +27,7 @@ class SessionRepository {
required List<TargetAnalysis> analyses,
String? notes,
int distance = 25,
DateTime? date,
}) async {
final session = Session(
id: id ?? _uuid.v4(),
@@ -34,7 +35,7 @@ class SessionRepository {
weaponId: weaponId,
maxShotsPerTarget: maxShotsPerTarget,
analyses: analyses,
createdAt: DateTime.now(),
createdAt: date ?? DateTime.now(),
notes: notes,
distance: distance,
);

View File

@@ -174,7 +174,7 @@ class AnalysisProvider extends ChangeNotifier {
x: impact.x,
y: impact.y,
score: impact.suggestedScore,
analysisId: '', // Will be set when saving
analysisId: '',
);
}).toList();
@@ -537,8 +537,7 @@ class AnalysisProvider extends ChangeNotifier {
_imagePath = correctedPath;
_correctedImagePath = correctedPath;
_distortionCorrectionEnabled = true;
_imageAspectRatio =
1.0; // The corrected image is always square (side x side)
_imageAspectRatio = 1.0;
notifyListeners();
}
@@ -616,18 +615,18 @@ class AnalysisProvider extends ChangeNotifier {
// 3. Application immédiate de la transformation (méthode asynchrone)
await applyDistortionCorrection();
} else {
notifyListeners(); // On prévient quand même si pas de correction
notifyListeners();
}
}
Future<void> runFullDistortionWorkflow() async {
_state = AnalysisState.loading; // Affiche un spinner sur votre UI
_state = AnalysisState.loading;
notifyListeners();
try {
calculateDistortion(); // Calcule les paramètres
await applyDistortionCorrection(); // Génère le fichier corrigé
_distortionCorrectionEnabled = true; // Active l'affichage
calculateDistortion();
await applyDistortionCorrection();
_distortionCorrectionEnabled = true;
_state = AnalysisState.success;
} catch (e) {
_errorMessage = "Erreur de rendu : $e";
@@ -688,12 +687,7 @@ class AnalysisProvider extends ChangeNotifier {
return false;
}
// Identifiant d'analyse temporaire
final analysisId = _shots.isNotEmpty && _shots.first.analysisId.isNotEmpty
? _shots.first.analysisId
: 'analysis_${DateTime.now().millisecondsSinceEpoch}';
final service = AiExportService(); // Local instanciation for simplicity
final service = AiExportService();
_state = AnalysisState.loading;
notifyListeners();
@@ -716,13 +710,14 @@ class AnalysisProvider extends ChangeNotifier {
return success;
}
/// Save the session (legacy standalone flow)
/// Save the session (Refactorisé pour appliquer le paramètre de date personnalisé)
Future<TargetAnalysis> saveSession({
String? notes,
String? sessionId,
String? weaponName,
String? weaponId,
int? distance,
DateTime? date, // <-- REÇOIT PROPREMENT LA DATE DE DEBUT DE SESSION
}) async {
if (_imagePath == null || _targetType == null) {
throw Exception('Cannot save: missing image or target type');
@@ -748,6 +743,7 @@ class AnalysisProvider extends ChangeNotifier {
if (existingSession != null) {
await _sessionRepository.addAnalysisToSession(sessionId, targetAnalysis);
} else {
// CORRECTION : Utilise la date injectée plutôt que DateTime.now()
await _sessionRepository.createSession(
id: sessionId,
weapon: weaponName ?? 'Inconnue',
@@ -756,9 +752,11 @@ class AnalysisProvider extends ChangeNotifier {
analyses: [targetAnalysis],
notes: notes,
distance: distance ?? 25,
date: date ?? DateTime.now(),
);
}
} else {
// CORRECTION : S'applique aussi au flux alternatif sans ID préalable
await _sessionRepository.createSession(
weapon: weaponName ?? 'Inconnue',
weaponId: weaponId,
@@ -766,6 +764,7 @@ class AnalysisProvider extends ChangeNotifier {
analyses: [targetAnalysis],
notes: notes,
distance: distance ?? 25,
date: date ?? DateTime.now(),
);
}

View File

@@ -568,7 +568,6 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
),
);
}
void _showSaveSessionDialog(BuildContext context, AnalysisProvider provider) {
showDialog(
context: context,
@@ -614,6 +613,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
weaponName: sessionProvider.currentWeapon,
weaponId: sessionProvider.currentWeaponId,
distance: sessionProvider.distance,
date: sessionProvider.sessionDate,
);
sessionProvider.addAnalysis(analysis);
@@ -645,6 +645,7 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
weaponName: sessionProvider.currentWeapon,
weaponId: sessionProvider.currentWeaponId,
distance: sessionProvider.distance,
date: sessionProvider.sessionDate, // <-- CORRECTION : Prise en compte de la date ici aussi !
);
if (context.mounted) {
@@ -666,4 +667,4 @@ class _AnalysisScreenContentState extends State<_AnalysisScreenContent> {
),
);
}
}
}

View File

@@ -4,6 +4,8 @@ import '../../data/models/target_analysis.dart';
import '../../data/models/weapon.dart';
class SessionProvider extends ChangeNotifier {
DateTime? _sessionDate;
DateTime? get sessionDate => _sessionDate;
String? _currentWeaponName;
String? _currentWeaponId;
int _shotsPerTarget = 5;
@@ -23,11 +25,12 @@ class SessionProvider extends ChangeNotifier {
int get totalSessionScore => _currentAnalyses.fold(0, (sum, a) => sum + a.totalScore);
int get targetCount => _currentAnalyses.length;
void startSession(String weaponName, int shots, String sessionId, {String? weaponId, int distance = 25}) {
void startSession(String weaponName, int shots, String sessionId, {String? weaponId, int distance = 25, DateTime? date}) {
_currentWeaponName = weaponName;
_currentWeaponId = weaponId;
_shotsPerTarget = shots;
_distance = distance;
_sessionDate = date ?? DateTime.now();
_activeSessionId = sessionId;
_currentAnalyses.clear();
_isSessionActive = true;

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart'; // Utile pour formater proprement la date en français
import '../../core/constants/app_constants.dart';
import '../../core/theme/app_theme.dart';
import '../../data/models/weapon.dart';
@@ -18,13 +19,15 @@ class SessionSetupScreen extends StatefulWidget {
class _SessionSetupScreenState extends State<SessionSetupScreen> {
final _formKey = GlobalKey<FormState>();
// Plus besoin du _weaponController, on force la sélection
int _shotsPerTarget = 5;
int _distance = 25;
List<Weapon> _availableWeapons = [];
Weapon? _selectedWeapon;
bool _isLoadingWeapons = true;
// AJOUT DE LA VARIABLE DATE : Initialisée par défaut à maintenant
DateTime _selectedDate = DateTime.now();
@override
void initState() {
super.initState();
@@ -39,7 +42,6 @@ class _SessionSetupScreenState extends State<SessionSetupScreen> {
if (mounted) {
setState(() {
_availableWeapons = weapons;
// Si la liste n'est pas vide et qu'aucune arme n'est sélectionnée, on sélectionne la première par défaut
if (_availableWeapons.isNotEmpty && _selectedWeapon == null) {
_selectedWeapon = _availableWeapons.first;
_updateSettingsForWeapon(_selectedWeapon!);
@@ -49,25 +51,47 @@ class _SessionSetupScreenState extends State<SessionSetupScreen> {
}
}
// Petite méthode pour mettre à jour les curseurs selon l'arme sélectionnée
void _updateSettingsForWeapon(Weapon weapon) {
_shotsPerTarget = weapon.magazineCapacity;
_distance = (weapon.type == WeaponType.handgun) ? 25 : 50;
}
// Fonction pour ouvrir le calendrier si l'utilisateur clique sur le champ
Future<void> _pickDate() async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _selectedDate,
firstDate: DateTime(2020),
lastDate: DateTime(2100),
locale: const Locale('fr', 'FR'), // Force le calendrier en français
);
if (picked != null && picked != _selectedDate) {
setState(() {
// On garde aussi l'heure actuelle lors du changement de date
final now = DateTime.now();
_selectedDate = DateTime(picked.year, picked.month, picked.day, now.hour, now.minute);
});
}
}
void _startSession() {
// Sécurité supplémentaire : impossible de démarrer sans arme
if (_formKey.currentState!.validate() && _selectedWeapon != null) {
final repository = context.read<SessionRepository>();
final sessionProvider = context.read<SessionProvider>();
final sessionId = repository.generateId();
// PASSE-PARTOUT : On envoie la session au provider.
// Note : Si ton `sessionProvider.startSession` ne prend pas encore la date en paramètre,
// pas de panique, la compilation passera car Dart tolère les arguments nommés optionnels
// s'ils sont déjà présents, ou tu pourras l'ajouter à ta méthode startSession.
sessionProvider.startSession(
_selectedWeapon!.displayName,
_shotsPerTarget,
sessionId,
weaponId: _selectedWeapon!.id,
distance: _distance,
date: _selectedDate,
);
Navigator.pushReplacement(
@@ -79,6 +103,9 @@ class _SessionSetupScreenState extends State<SessionSetupScreen> {
@override
Widget build(BuildContext context) {
// Petit formatage sympa en français (ex: "27 mai 2026")
final String formattedDate = DateFormat('dd MMMM yyyy', 'fr_FR').format(_selectedDate);
return Scaffold(
appBar: AppBar(
title: const Text('Configuration de la session'),
@@ -93,7 +120,7 @@ class _SessionSetupScreenState extends State<SessionSetupScreen> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Choix de l\'arme',
'Informations générales',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
@@ -128,7 +155,6 @@ class _SessionSetupScreenState extends State<SessionSetupScreen> {
context,
MaterialPageRoute(builder: (_) => const WeaponListScreen()),
).then((_) {
// Quand on revient de l'armurerie, on recharge la liste !
_loadWeapons();
});
},
@@ -139,28 +165,50 @@ class _SessionSetupScreenState extends State<SessionSetupScreen> {
),
)
else
DropdownButtonFormField<Weapon>(
value: _selectedWeapon,
decoration: InputDecoration(
labelText: 'Sélectionner une arme',
prefixIcon: const Icon(Icons.shield),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
Column(
children: [
DropdownButtonFormField<Weapon>(
value: _selectedWeapon,
decoration: InputDecoration(
labelText: 'Sélectionner une arme',
prefixIcon: const Icon(Icons.shield),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
),
),
items: _availableWeapons.map((w) => DropdownMenuItem(
value: w,
child: Text(w.displayName),
)).toList(),
onChanged: (value) {
if (value != null) {
setState(() {
_selectedWeapon = value;
_updateSettingsForWeapon(value);
});
}
},
),
),
// On a retiré le choix "null / Autre", l'utilisateur DOIT choisir une arme existante
items: _availableWeapons.map((w) => DropdownMenuItem(
value: w,
child: Text(w.displayName),
)).toList(),
onChanged: (value) {
if (value != null) {
setState(() {
_selectedWeapon = value;
_updateSettingsForWeapon(value);
});
}
},
const SizedBox(height: 16),
// NOUVEAU CHAMP : Sélecteur de date interactif
InkWell(
onTap: _availableWeapons.isEmpty ? null : _pickDate,
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
child: IgnorePointer(
child: TextFormField(
decoration: InputDecoration(
labelText: 'Date de la session',
prefixIcon: const Icon(Icons.calendar_today),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
),
),
controller: TextEditingController(text: formattedDate),
),
),
),
],
),
const SizedBox(height: 24),
@@ -252,7 +300,6 @@ class _SessionSetupScreenState extends State<SessionSetupScreen> {
const SizedBox(height: 48),
ElevatedButton.icon(
// On désactive le bouton si la liste est vide ou si aucune arme n'est sélectionnée
onPressed: (_availableWeapons.isEmpty || _selectedWeapon == null)
? null
: _startSession,