Files
impact/lib/features/session/session_setup_screen.dart

535 lines
17 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';
import '../../core/constants/app_constants.dart';
import '../../core/theme/app_theme.dart';
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});
@override
State<SessionSetupScreen> createState() => _SessionSetupScreenState();
}
class _SessionSetupScreenState extends State<SessionSetupScreen> {
final _formKey = GlobalKey<FormState>();
int _shotsPerTarget = 5;
int _distance = 25;
List<Weapon> _availableWeapons = [];
Weapon? _selectedWeapon;
bool _isLoadingWeapons = true;
DateTime _selectedDate = DateTime.now();
/// Valeur sentinelle de l'item "Ajouter une nouvelle arme" du menu deroulant.
static const String _addWeaponValue = '__add_weapon__';
final GlobalKey<FormFieldState<String>> _weaponFieldKey =
GlobalKey<FormFieldState<String>>();
static const List<int> _presetDistances = [10, 15, 25, 50, 100, 200];
static const List<int> _presetShots = [3, 5, 10, 15, 20, 30, 50];
@override
void initState() {
super.initState();
_loadWeapons();
}
Future<void> _loadWeapons() async {
setState(() => _isLoadingWeapons = true);
final repository = context.read<SessionRepository>();
final weapons = await repository.getWeapons();
if (mounted) {
setState(() {
_availableWeapons = weapons;
// Les armes rechargees sont de nouvelles instances (Weapon n'a pas
// d'operator ==) et l'arme selectionnee a pu etre supprimee : on
// re-resout la selection par identifiant.
final previousId = _selectedWeapon?.id;
_selectedWeapon = null;
for (final weapon in weapons) {
if (weapon.id == previousId) {
_selectedWeapon = weapon;
break;
}
}
if (_selectedWeapon == null && weapons.isNotEmpty) {
_selectedWeapon = weapons.first;
_updateSettingsForWeapon(_selectedWeapon!);
}
_isLoadingWeapons = false;
});
}
}
Future<void> _openArmory() async {
final knownIds = _availableWeapons.map((w) => w.id).toSet();
await Navigator.push(
context,
MaterialPageRoute(builder: (_) => const WeaponListScreen()),
);
if (!mounted) return;
await _loadWeapons();
if (!mounted) return;
// Selectionne automatiquement l'arme qui vient d'etre ajoutee.
Weapon? added;
for (final weapon in _availableWeapons) {
if (!knownIds.contains(weapon.id)) {
added = weapon;
break;
}
}
if (added != null) {
final newWeapon = added;
setState(() {
_selectedWeapon = newWeapon;
_updateSettingsForWeapon(newWeapon);
});
}
}
void _updateSettingsForWeapon(Weapon weapon) {
_shotsPerTarget = weapon.magazineCapacity > 0 ? weapon.magazineCapacity : 5;
_distance = (weapon.type == WeaponType.handgun) ? 25 : 50;
}
Future<void> _pickDate() async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _selectedDate,
firstDate: DateTime(2020),
lastDate: DateTime(2100),
locale: const Locale('fr', 'FR'),
);
if (picked != null && picked != _selectedDate) {
setState(() {
final now = DateTime.now();
_selectedDate = DateTime(
picked.year,
picked.month,
picked.day,
now.hour,
now.minute,
);
});
}
}
void _startSession() {
if (_formKey.currentState!.validate() && _selectedWeapon != null) {
final repository = context.read<SessionRepository>();
final sessionProvider = context.read<SessionProvider>();
final sessionId = repository.generateId();
sessionProvider.startSession(
_selectedWeapon!.displayName,
_shotsPerTarget,
sessionId,
weaponId: _selectedWeapon!.id,
distance: _distance,
date: _selectedDate,
);
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const CaptureScreen()),
);
}
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final formattedDate = DateFormat('dd MMMM yyyy • HH:mm', 'fr_FR').format(_selectedDate);
return Scaffold(
appBar: AppBar(
title: const Text('Configuration de Session'),
),
body: _isLoadingWeapons
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.all(AppConstants.defaultPadding),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (_availableWeapons.isEmpty)
_buildEmptyArmoryState(isDark)
else ...[
_buildWeaponAndDateSection(isDark, formattedDate),
const SizedBox(height: 20),
_buildDistanceSection(isDark),
const SizedBox(height: 20),
_buildShotsSection(isDark),
const SizedBox(height: 32),
_buildStartButton(),
const SizedBox(height: 16),
],
],
),
),
),
);
}
Widget _buildEmptyArmoryState(bool isDark) {
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: isDark ? AppTheme.darkSurface : AppTheme.lightSurface,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppTheme.errorColor.withValues(alpha: 0.5),
),
),
child: Column(
children: [
const Icon(
Icons.warning_amber_rounded,
color: AppTheme.errorColor,
size: 52,
),
const SizedBox(height: 12),
Text(
'Armurerie vide',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: isDark ? AppTheme.darkTextPrimary : AppTheme.lightTextPrimary,
),
),
const SizedBox(height: 6),
Text(
'Ajoutez au moins une arme dans votre armurerie pour débuter une séance.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: isDark ? AppTheme.darkTextSecondary : AppTheme.lightTextSecondary,
),
),
const SizedBox(height: 20),
ElevatedButton.icon(
onPressed: _openArmory,
icon: const Icon(Icons.shield),
label: const Text('Aller à l\'armurerie'),
),
],
),
);
}
Widget _buildWeaponAndDateSection(bool isDark, String formattedDate) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.tune, color: AppTheme.primaryColor, size: 20),
const SizedBox(width: 8),
Text(
'Arme & Date',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: isDark ? AppTheme.darkTextPrimary : AppTheme.lightTextPrimary,
),
),
],
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
key: _weaponFieldKey,
initialValue: _selectedWeapon?.id,
decoration: const InputDecoration(
labelText: 'Arme utilisée',
prefixIcon: Icon(Icons.shield_outlined),
),
items: [
..._availableWeapons.map(
(w) => DropdownMenuItem(
value: w.id,
child: Text('${w.displayName} (${w.caliber})'),
),
),
DropdownMenuItem(
value: _addWeaponValue,
child: Row(
children: [
Icon(
Icons.add_circle_outline,
size: 18,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(
'Ajouter une nouvelle arme',
style: TextStyle(
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.primary,
),
),
],
),
),
],
onChanged: (value) {
if (value == null) return;
if (value == _addWeaponValue) {
// L'item d'action n'est pas une arme : on restaure aussitot
// la selection precedente avant d'ouvrir l'armurerie.
_weaponFieldKey.currentState?.didChange(_selectedWeapon?.id);
_openArmory();
return;
}
for (final weapon in _availableWeapons) {
if (weapon.id == value) {
setState(() {
_selectedWeapon = weapon;
_updateSettingsForWeapon(weapon);
});
break;
}
}
},
),
const SizedBox(height: 12),
InkWell(
onTap: _pickDate,
borderRadius: BorderRadius.circular(12),
child: IgnorePointer(
child: TextFormField(
decoration: const InputDecoration(
labelText: 'Date de séance',
prefixIcon: Icon(Icons.calendar_today_outlined),
),
controller: TextEditingController(text: formattedDate),
),
),
),
],
),
),
);
}
Widget _buildDistanceSection(bool isDark) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const Icon(Icons.straighten, color: AppTheme.secondaryColor, size: 20),
const SizedBox(width: 8),
Text(
'Distance de Tir',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: isDark ? AppTheme.darkTextPrimary : AppTheme.lightTextPrimary,
),
),
],
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: AppTheme.secondaryColor.withValues(alpha: isDark ? 0.2 : 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppTheme.secondaryColor.withValues(alpha: 0.4),
),
),
child: Text(
'${_distance}m',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w800,
color: AppTheme.secondaryColor,
),
),
),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: _presetDistances.map((d) {
final isSelected = _distance == d;
return ChoiceChip(
label: Text('${d}m'),
selected: isSelected,
onSelected: (selected) {
if (selected) setState(() => _distance = d);
},
);
}).toList(),
),
const SizedBox(height: 8),
Slider(
value: _distance.toDouble(),
min: 5,
max: 300,
divisions: 59,
activeColor: AppTheme.secondaryColor,
onChanged: (val) {
setState(() => _distance = val.round());
},
),
],
),
),
);
}
Widget _buildShotsSection(bool isDark) {
final primaryColor = Theme.of(context).colorScheme.primary;
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(Icons.ads_click, color: primaryColor, size: 20),
const SizedBox(width: 8),
Text(
'Tirs par Cible',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
color: isDark ? AppTheme.darkTextPrimary : AppTheme.lightTextPrimary,
),
),
],
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: primaryColor.withValues(alpha: isDark ? 0.2 : 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: primaryColor.withValues(alpha: 0.4),
),
),
child: Text(
'$_shotsPerTarget tirs',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w800,
color: primaryColor,
),
),
),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: _presetShots.map((s) {
final isSelected = _shotsPerTarget == s;
return ChoiceChip(
label: Text('$s coups'),
selected: isSelected,
onSelected: (selected) {
if (selected) setState(() => _shotsPerTarget = s);
},
);
}).toList(),
),
const SizedBox(height: 8),
Slider(
value: _shotsPerTarget.toDouble(),
min: 1,
max: 50,
divisions: 49,
activeColor: primaryColor,
onChanged: (val) {
setState(() => _shotsPerTarget = val.round());
},
),
],
),
),
);
}
Widget _buildStartButton() {
final primaryColor = Theme.of(context).colorScheme.primary;
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: LinearGradient(
colors: [
primaryColor,
HSLColor.fromColor(primaryColor).withLightness((HSLColor.fromColor(primaryColor).lightness + 0.15).clamp(0.0, 1.0)).toColor(),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: primaryColor.withValues(alpha: 0.35),
blurRadius: 16,
offset: const Offset(0, 4),
),
],
),
child: ElevatedButton.icon(
onPressed: (_availableWeapons.isEmpty || _selectedWeapon == null)
? null
: _startSession,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,
shadowColor: Colors.transparent,
padding: const EdgeInsets.symmetric(vertical: 18),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
icon: const Icon(Icons.play_arrow_rounded, size: 26),
label: const Text(
'DÉMARRER LA SESSION',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w800,
letterSpacing: 0.8,
),
),
),
);
}
}