From 2f493ef622f0f6d04fe601ba39dd9c91d376b670 Mon Sep 17 00:00:00 2001 From: qguillaume Date: Tue, 26 May 2026 22:41:44 +0200 Subject: [PATCH] =?UTF-8?q?feat(setup):=20choix=20d'arme=20obligatoire,=20?= =?UTF-8?q?redirection=20armurerie=20et=20max=20balles=20=C3=A0=2050?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../session/session_setup_screen.dart | 154 ++++++++++-------- 1 file changed, 87 insertions(+), 67 deletions(-) diff --git a/lib/features/session/session_setup_screen.dart b/lib/features/session/session_setup_screen.dart index 4aab7c0b..9c54af7d 100644 --- a/lib/features/session/session_setup_screen.dart +++ b/lib/features/session/session_setup_screen.dart @@ -6,6 +6,7 @@ import '../../data/models/weapon.dart'; import '../../data/repositories/session_repository.dart'; import 'session_provider.dart'; import '../capture/capture_screen.dart'; +import '../garage/weapon_list_screen.dart'; class SessionSetupScreen extends StatefulWidget { const SessionSetupScreen({super.key}); @@ -16,7 +17,8 @@ class SessionSetupScreen extends StatefulWidget { class _SessionSetupScreenState extends State { final _formKey = GlobalKey(); - final _weaponController = TextEditingController(); + + // Plus besoin du _weaponController, on force la sélection int _shotsPerTarget = 5; int _distance = 25; List _availableWeapons = []; @@ -30,33 +32,41 @@ class _SessionSetupScreenState extends State { } Future _loadWeapons() async { + setState(() => _isLoadingWeapons = true); final repository = context.read(); final weapons = await repository.getWeapons(); + 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!); + } _isLoadingWeapons = false; }); } } - @override - void dispose() { - _weaponController.dispose(); - super.dispose(); + // 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; } 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(); final sessionProvider = context.read(); - + final sessionId = repository.generateId(); sessionProvider.startSession( - _selectedWeapon != null ? _selectedWeapon!.displayName : _weaponController.text, + _selectedWeapon!.displayName, _shotsPerTarget, sessionId, - weaponId: _selectedWeapon?.id, + weaponId: _selectedWeapon!.id, distance: _distance, ); @@ -73,7 +83,7 @@ class _SessionSetupScreenState extends State { appBar: AppBar( title: const Text('Configuration de la session'), ), - body: _isLoadingWeapons + body: _isLoadingWeapons ? const Center(child: CircularProgressIndicator()) : SingleChildScrollView( padding: const EdgeInsets.all(AppConstants.defaultPadding), @@ -87,65 +97,72 @@ class _SessionSetupScreenState extends State { style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), const SizedBox(height: 16), - - if (_availableWeapons.isNotEmpty) ...[ - DropdownButtonFormField( + + // AFFICHAGE CONDITIONNEL : Armurerie vide vs Armurerie remplie + 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( value: _selectedWeapon, decoration: InputDecoration( - labelText: 'Sélectionner une arme de l\'armurerie', + labelText: 'Sélectionner une arme', prefixIcon: const Icon(Icons.shield), border: OutlineInputBorder( borderRadius: BorderRadius.circular(AppConstants.borderRadius), ), ), - items: [ - const DropdownMenuItem( - value: null, - child: Text('Nouvelle arme / Autre'), - ), - ..._availableWeapons.map((w) => DropdownMenuItem( - value: w, - child: Text(w.displayName), - )), - ], + // 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) { - setState(() { - _selectedWeapon = value; - if (value != null) { - _weaponController.text = value.displayName; - _shotsPerTarget = value.magazineCapacity; - // Auto-set distance based on weapon type if needed - _distance = (value.type == WeaponType.handgun) ? 25 : 50; - } else { - _weaponController.clear(); - } - }); + if (value != null) { + setState(() { + _selectedWeapon = value; + _updateSettingsForWeapon(value); + }); + } }, ), - 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), // Distance selector @@ -161,9 +178,9 @@ class _SessionSetupScreenState extends State { value: _distance.toDouble(), min: 5, max: 300, - divisions: 59, // (300-5)/5 = 59 divisions for 5m steps + divisions: 59, label: '${_distance}m', - onChanged: (value) { + onChanged: _availableWeapons.isEmpty ? null : (value) { setState(() { _distance = value.round(); }); @@ -190,7 +207,7 @@ class _SessionSetupScreenState extends State { ], ), const SizedBox(height: 24), - + // Shots per target const Text( 'Nombre de balles pour la cible actuelle', @@ -203,10 +220,10 @@ class _SessionSetupScreenState extends State { child: Slider( value: _shotsPerTarget.toDouble(), min: 1, - max: 30, - divisions: 29, + max: 50, + divisions: 49, label: '$_shotsPerTarget', - onChanged: (value) { + onChanged: _availableWeapons.isEmpty ? null : (value) { setState(() { _shotsPerTarget = value.round(); }); @@ -233,9 +250,12 @@ class _SessionSetupScreenState extends State { ], ), const SizedBox(height: 48), - + 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( backgroundColor: AppTheme.primaryColor, foregroundColor: Colors.white, @@ -256,4 +276,4 @@ class _SessionSetupScreenState extends State { ), ); } -} +} \ No newline at end of file