feat(session): ajout du sélecteur de date et persistance dans l'historique de tir
This commit is contained in:
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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> {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user