feat(setup): choix d'arme obligatoire, redirection armurerie et max balles à 50

This commit is contained in:
qguillaume
2026-05-26 22:41:44 +02:00
parent 33e1522df0
commit 2f493ef622

View File

@@ -6,6 +6,7 @@ import '../../data/models/weapon.dart';
import '../../data/repositories/session_repository.dart'; import '../../data/repositories/session_repository.dart';
import 'session_provider.dart'; import 'session_provider.dart';
import '../capture/capture_screen.dart'; import '../capture/capture_screen.dart';
import '../garage/weapon_list_screen.dart';
class SessionSetupScreen extends StatefulWidget { class SessionSetupScreen extends StatefulWidget {
const SessionSetupScreen({super.key}); const SessionSetupScreen({super.key});
@@ -16,7 +17,8 @@ class SessionSetupScreen extends StatefulWidget {
class _SessionSetupScreenState extends State<SessionSetupScreen> { class _SessionSetupScreenState extends State<SessionSetupScreen> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _weaponController = TextEditingController();
// Plus besoin du _weaponController, on force la sélection
int _shotsPerTarget = 5; int _shotsPerTarget = 5;
int _distance = 25; int _distance = 25;
List<Weapon> _availableWeapons = []; List<Weapon> _availableWeapons = [];
@@ -30,33 +32,41 @@ class _SessionSetupScreenState extends State<SessionSetupScreen> {
} }
Future<void> _loadWeapons() async { Future<void> _loadWeapons() async {
setState(() => _isLoadingWeapons = true);
final repository = context.read<SessionRepository>(); final repository = context.read<SessionRepository>();
final weapons = await repository.getWeapons(); final weapons = await repository.getWeapons();
if (mounted) { if (mounted) {
setState(() { setState(() {
_availableWeapons = weapons; _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!);
}
_isLoadingWeapons = false; _isLoadingWeapons = false;
}); });
} }
} }
@override // Petite méthode pour mettre à jour les curseurs selon l'arme sélectionnée
void dispose() { void _updateSettingsForWeapon(Weapon weapon) {
_weaponController.dispose(); _shotsPerTarget = weapon.magazineCapacity;
super.dispose(); _distance = (weapon.type == WeaponType.handgun) ? 25 : 50;
} }
void _startSession() { void _startSession() {
if (_formKey.currentState!.validate()) { // Sécurité supplémentaire : impossible de démarrer sans arme
if (_formKey.currentState!.validate() && _selectedWeapon != null) {
final repository = context.read<SessionRepository>(); final repository = context.read<SessionRepository>();
final sessionProvider = context.read<SessionProvider>(); final sessionProvider = context.read<SessionProvider>();
final sessionId = repository.generateId(); final sessionId = repository.generateId();
sessionProvider.startSession( sessionProvider.startSession(
_selectedWeapon != null ? _selectedWeapon!.displayName : _weaponController.text, _selectedWeapon!.displayName,
_shotsPerTarget, _shotsPerTarget,
sessionId, sessionId,
weaponId: _selectedWeapon?.id, weaponId: _selectedWeapon!.id,
distance: _distance, distance: _distance,
); );
@@ -88,64 +98,71 @@ class _SessionSetupScreenState extends State<SessionSetupScreen> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
if (_availableWeapons.isNotEmpty) ...[ // AFFICHAGE CONDITIONNEL : Armurerie vide vs Armurerie remplie
DropdownButtonFormField<Weapon?>( if (_availableWeapons.isEmpty)
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.errorColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
border: Border.all(color: AppTheme.errorColor.withValues(alpha: 0.5)),
),
child: Column(
children: [
const Icon(Icons.warning_amber_rounded, color: AppTheme.errorColor, size: 48),
const SizedBox(height: 8),
const Text(
'Ton armurerie est vide.',
style: TextStyle(fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
const Text(
'Tu dois d\'abord ajouter une arme pour pouvoir démarrer une session de tir.',
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const WeaponListScreen()),
).then((_) {
// Quand on revient de l'armurerie, on recharge la liste !
_loadWeapons();
});
},
icon: const Icon(Icons.add),
label: const Text('ALLER À MON ARMURERIE'),
),
],
),
)
else
DropdownButtonFormField<Weapon>(
value: _selectedWeapon, value: _selectedWeapon,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Sélectionner une arme de l\'armurerie', labelText: 'Sélectionner une arme',
prefixIcon: const Icon(Icons.shield), prefixIcon: const Icon(Icons.shield),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppConstants.borderRadius), borderRadius: BorderRadius.circular(AppConstants.borderRadius),
), ),
), ),
items: [ // On a retiré le choix "null / Autre", l'utilisateur DOIT choisir une arme existante
const DropdownMenuItem<Weapon?>( items: _availableWeapons.map((w) => DropdownMenuItem(
value: null, value: w,
child: Text('Nouvelle arme / Autre'), child: Text(w.displayName),
), )).toList(),
..._availableWeapons.map((w) => DropdownMenuItem(
value: w,
child: Text(w.displayName),
)),
],
onChanged: (value) { onChanged: (value) {
setState(() { if (value != null) {
_selectedWeapon = value; setState(() {
if (value != null) { _selectedWeapon = value;
_weaponController.text = value.displayName; _updateSettingsForWeapon(value);
_shotsPerTarget = value.magazineCapacity; });
// Auto-set distance based on weapon type if needed }
_distance = (value.type == WeaponType.handgun) ? 25 : 50;
} else {
_weaponController.clear();
}
});
}, },
), ),
const SizedBox(height: 16),
const Center(child: Text('OU', style: TextStyle(color: Colors.grey, fontWeight: FontWeight.bold))),
const SizedBox(height: 16),
],
// Weapon field (manual entry if no weapon selected)
TextFormField(
controller: _weaponController,
enabled: _selectedWeapon == null,
decoration: InputDecoration(
labelText: 'Nom de l\'arme',
hintText: 'ex: Glock 17, CZ Shadow 2...',
prefixIcon: const Icon(Icons.edit),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez renseigner votre arme';
}
return null;
},
),
const SizedBox(height: 24), const SizedBox(height: 24),
// Distance selector // Distance selector
@@ -161,9 +178,9 @@ class _SessionSetupScreenState extends State<SessionSetupScreen> {
value: _distance.toDouble(), value: _distance.toDouble(),
min: 5, min: 5,
max: 300, max: 300,
divisions: 59, // (300-5)/5 = 59 divisions for 5m steps divisions: 59,
label: '${_distance}m', label: '${_distance}m',
onChanged: (value) { onChanged: _availableWeapons.isEmpty ? null : (value) {
setState(() { setState(() {
_distance = value.round(); _distance = value.round();
}); });
@@ -203,10 +220,10 @@ class _SessionSetupScreenState extends State<SessionSetupScreen> {
child: Slider( child: Slider(
value: _shotsPerTarget.toDouble(), value: _shotsPerTarget.toDouble(),
min: 1, min: 1,
max: 30, max: 50,
divisions: 29, divisions: 49,
label: '$_shotsPerTarget', label: '$_shotsPerTarget',
onChanged: (value) { onChanged: _availableWeapons.isEmpty ? null : (value) {
setState(() { setState(() {
_shotsPerTarget = value.round(); _shotsPerTarget = value.round();
}); });
@@ -235,7 +252,10 @@ class _SessionSetupScreenState extends State<SessionSetupScreen> {
const SizedBox(height: 48), const SizedBox(height: 48),
ElevatedButton.icon( ElevatedButton.icon(
onPressed: _startSession, // 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,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor, backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white, foregroundColor: Colors.white,