feat(setup): choix d'arme obligatoire, redirection armurerie et max balles à 50
This commit is contained in:
@@ -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,
|
|
||||||
child: Text('Nouvelle arme / Autre'),
|
|
||||||
),
|
|
||||||
..._availableWeapons.map((w) => DropdownMenuItem(
|
|
||||||
value: w,
|
value: w,
|
||||||
child: Text(w.displayName),
|
child: Text(w.displayName),
|
||||||
)),
|
)).toList(),
|
||||||
],
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedWeapon = value;
|
_selectedWeapon = value;
|
||||||
if (value != null) {
|
_updateSettingsForWeapon(value);
|
||||||
_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();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
|
||||||
),
|
|
||||||
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,
|
||||||
|
|||||||
Reference in New Issue
Block a user