feat: accessoires en armurerie, comparateur de sessions stats, croix de centrage pixel par pixel
This commit is contained in:
@@ -337,24 +337,41 @@ class TargetCalibrationState extends State<TargetCalibration> {
|
|||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: Colors.white10),
|
border: Border.all(color: Colors.white10),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
_buildDirectionButton(Icons.keyboard_arrow_up, () => _moveCenterByPixels(0, -1, size)),
|
_buildSignLabel('−'),
|
||||||
Row(
|
const SizedBox(width: 12),
|
||||||
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
_buildDirectionButton(Icons.keyboard_arrow_left, () => _moveCenterByPixels(-1, 0, size)),
|
_buildDirectionButton(Icons.keyboard_arrow_up, () => _moveCenterByPixels(0, -1, size)),
|
||||||
const SizedBox(width: 40),
|
Row(
|
||||||
_buildDirectionButton(Icons.keyboard_arrow_right, () => _moveCenterByPixels(1, 0, size)),
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
_buildDirectionButton(Icons.keyboard_arrow_left, () => _moveCenterByPixels(-1, 0, size)),
|
||||||
|
const SizedBox(width: 40),
|
||||||
|
_buildDirectionButton(Icons.keyboard_arrow_right, () => _moveCenterByPixels(1, 0, size)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
_buildDirectionButton(Icons.keyboard_arrow_down, () => _moveCenterByPixels(0, 1, size)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
_buildDirectionButton(Icons.keyboard_arrow_down, () => _moveCenterByPixels(0, 1, size)),
|
const SizedBox(width: 12),
|
||||||
|
_buildSignLabel('+'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Symbole purement décoratif affiché de part et d'autre de la croix.
|
||||||
|
Widget _buildSignLabel(String text) {
|
||||||
|
return Text(
|
||||||
|
text,
|
||||||
|
style: const TextStyle(color: Colors.white54, fontSize: 28, fontWeight: FontWeight.bold),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildDirectionButton(IconData icon, VoidCallback onPressed) {
|
Widget _buildDirectionButton(IconData icon, VoidCallback onPressed) {
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(color: Colors.black87, borderRadius: BorderRadius.circular(8)),
|
decoration: BoxDecoration(color: Colors.black87, borderRadius: BorderRadius.circular(8)),
|
||||||
|
|||||||
@@ -101,6 +101,33 @@ class _CropScreenState extends State<CropScreen> {
|
|||||||
? const Center(child: CircularProgressIndicator(color: Color(0xFF1A73E8)))
|
? const Center(child: CircularProgressIndicator(color: Color(0xFF1A73E8)))
|
||||||
: Column(
|
: Column(
|
||||||
children: [
|
children: [
|
||||||
|
// TEXTE D'AIDE — placé en haut, sous le titre, au-dessus de l'image.
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 12, 20, 4),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.center_focus_strong, color: Color(0xFF00FF00), size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
'Alignez et pivotez la cible sur la croix',
|
||||||
|
style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 14, fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Glissez à un doigt pour déplacer, pincez pour zoomer',
|
||||||
|
style: TextStyle(color: Colors.white.withOpacity(0.5), fontSize: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// Zone interactive de crop, CARRÉE. La photo la remplit entièrement
|
// Zone interactive de crop, CARRÉE. La photo la remplit entièrement
|
||||||
// (BoxFit.cover) → aucun bord noir dans la zone. Le débord hors cadre
|
// (BoxFit.cover) → aucun bord noir dans la zone. Le débord hors cadre
|
||||||
// est récupérable en déplaçant/zoomant. La sortie d'analyse reste
|
// est récupérable en déplaçant/zoomant. La sortie d'analyse reste
|
||||||
@@ -126,30 +153,10 @@ class _CropScreenState extends State<CropScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// TEXTE D'AIDE AJUSTÉ
|
const SizedBox(height: 8),
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
// CROIX DIRECTIONNELLE — déplace la photo pixel par pixel.
|
||||||
child: Column(
|
_buildDirectionalPad(),
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.center_focus_strong, color: Color(0xFF00FF00), size: 20),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
'Alignez et pivotez la cible sur la croix',
|
|
||||||
style: TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 14, fontWeight: FontWeight.w500),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
'Glissez à un doigt pour déplacer, pincez pour zoomer',
|
|
||||||
style: TextStyle(color: Colors.white.withOpacity(0.5), fontSize: 12),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
@@ -355,6 +362,61 @@ class _CropScreenState extends State<CropScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Croix directionnelle compacte pour déplacer la photo pixel par pixel.
|
||||||
|
// Les symboles « − » et « + » de part et d'autre sont purement décoratifs.
|
||||||
|
Widget _buildDirectionalPad() {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
_buildCropSignLabel('−'),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
_buildCropDirButton(Icons.keyboard_arrow_up, () => _nudge(0, -1)),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
_buildCropDirButton(Icons.keyboard_arrow_left, () => _nudge(-1, 0)),
|
||||||
|
const SizedBox(width: 28),
|
||||||
|
_buildCropDirButton(Icons.keyboard_arrow_right, () => _nudge(1, 0)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
_buildCropDirButton(Icons.keyboard_arrow_down, () => _nudge(0, 1)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
_buildCropSignLabel('+'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCropDirButton(IconData icon, VoidCallback onPressed) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(color: Colors.black54, borderRadius: BorderRadius.circular(6)),
|
||||||
|
margin: const EdgeInsets.all(2),
|
||||||
|
child: IconButton(
|
||||||
|
icon: Icon(icon, color: Colors.white, size: 22),
|
||||||
|
onPressed: onPressed,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCropSignLabel(String text) {
|
||||||
|
return Text(
|
||||||
|
text,
|
||||||
|
style: const TextStyle(color: Colors.white54, fontSize: 24, fontWeight: FontWeight.bold),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _nudge(double dx, double dy) {
|
||||||
|
setState(() {
|
||||||
|
_offset = _offset + Offset(dx, dy);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void _onScaleStart(ScaleStartDetails details) {
|
void _onScaleStart(ScaleStartDetails details) {
|
||||||
_baseScale = _scale;
|
_baseScale = _scale;
|
||||||
_startFocalPoint = details.focalPoint;
|
_startFocalPoint = details.focalPoint;
|
||||||
|
|||||||
@@ -76,26 +76,11 @@ class _WeaponListScreenState extends State<WeaponListScreen> {
|
|||||||
itemCount: _weapons.length,
|
itemCount: _weapons.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final weapon = _weapons[index];
|
final weapon = _weapons[index];
|
||||||
|
final accessories = _accessoryChips(weapon);
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
child: ListTile(
|
child: InkWell(
|
||||||
leading: CircleAvatar(
|
borderRadius: BorderRadius.circular(12),
|
||||||
backgroundColor: AppTheme.primaryColor.withValues(alpha: 0.1),
|
|
||||||
child: Icon(
|
|
||||||
weapon.type == WeaponType.handgun ? Icons.shield : Icons.ads_click,
|
|
||||||
color: AppTheme.primaryColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(weapon.displayName, style: const TextStyle(fontWeight: FontWeight.bold)),
|
|
||||||
subtitle: Text('${weapon.type.displayName} • ${weapon.caliber}'),
|
|
||||||
trailing: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Text('${weapon.magazineCount} chargeurs', style: const TextStyle(fontSize: 12)),
|
|
||||||
Text('${weapon.magazineCapacity} coups', style: const TextStyle(fontSize: 12, color: Colors.grey)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await Navigator.push(
|
await Navigator.push(
|
||||||
context,
|
context,
|
||||||
@@ -104,12 +89,96 @@ class _WeaponListScreenState extends State<WeaponListScreen> {
|
|||||||
_loadWeapons(); // Reload in case it was edited or maintenance was added
|
_loadWeapons(); // Reload in case it was edited or maintenance was added
|
||||||
},
|
},
|
||||||
onLongPress: () => _confirmDelete(context, weapon),
|
onLongPress: () => _confirmDelete(context, weapon),
|
||||||
|
child: Padding(
|
||||||
|
// La hauteur du cadre s'adapte automatiquement à la liste d'accessoires.
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
backgroundColor: AppTheme.primaryColor.withValues(alpha: 0.1),
|
||||||
|
child: Icon(
|
||||||
|
weapon.type == WeaponType.handgun ? Icons.shield : Icons.ads_click,
|
||||||
|
color: AppTheme.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(weapon.displayName, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
if (accessories.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Wrap(
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 6,
|
||||||
|
children: accessories,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
] else
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
'${weapon.type.displayName} • ${weapon.caliber}',
|
||||||
|
style: const TextStyle(fontSize: 13, color: Colors.grey),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text('${weapon.magazineCount} chargeurs', style: const TextStyle(fontSize: 12)),
|
||||||
|
Text('${weapon.magazineCapacity} coups', style: const TextStyle(fontSize: 12, color: Colors.grey)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Construit la liste des "puces" d'accessoires renseignés pour une arme.
|
||||||
|
// Seuls les accessoires effectivement définis sont affichés ; la card
|
||||||
|
// s'agrandit donc en fonction du nombre d'accessoires.
|
||||||
|
List<Widget> _accessoryChips(Weapon weapon) {
|
||||||
|
final items = <(IconData, String)>[];
|
||||||
|
if (weapon.optic != null && weapon.optic!.isNotEmpty) {
|
||||||
|
items.add((Icons.center_focus_strong, weapon.optic!));
|
||||||
|
}
|
||||||
|
if (weapon.silencer != null && weapon.silencer!.isNotEmpty) {
|
||||||
|
items.add((Icons.volume_off, weapon.silencer!));
|
||||||
|
}
|
||||||
|
if (weapon.trigger != null && weapon.trigger!.isNotEmpty) {
|
||||||
|
items.add((Icons.touch_app, weapon.trigger!));
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.map((item) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.primaryColor.withValues(alpha: 0.08),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: AppTheme.primaryColor.withValues(alpha: 0.25)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(item.$1, size: 13, color: AppTheme.primaryColor),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(item.$2, style: const TextStyle(fontSize: 12)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
void _showAddWeaponDialog(BuildContext context) async {
|
void _showAddWeaponDialog(BuildContext context) async {
|
||||||
final nameController = TextEditingController();
|
final nameController = TextEditingController();
|
||||||
final caliberController = TextEditingController();
|
final caliberController = TextEditingController();
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ class _StatisticsScreenState extends State<StatisticsScreen> {
|
|||||||
List<String> _availableWeapons = ['Toutes'];
|
List<String> _availableWeapons = ['Toutes'];
|
||||||
List<String> _availableDistances = ['Toutes'];
|
List<String> _availableDistances = ['Toutes'];
|
||||||
|
|
||||||
|
// --- Comparateur de sessions ---
|
||||||
|
// Quand 2 sessions sont sélectionnées, l'écran passe en mode comparaison :
|
||||||
|
// un switch permet d'alterner l'affichage des stats entre la session A et B.
|
||||||
|
Session? _compareA;
|
||||||
|
Session? _compareB;
|
||||||
|
bool _showingB = false; // false = on affiche A, true = on affiche B
|
||||||
|
bool get _compareMode => _compareA != null && _compareB != null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -80,6 +88,17 @@ class _StatisticsScreenState extends State<StatisticsScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _calculateStats() {
|
void _calculateStats() {
|
||||||
|
// Mode comparaison : on calcule les stats sur la seule session active
|
||||||
|
// (A ou B selon le switch), sans filtre de période.
|
||||||
|
if (_compareMode) {
|
||||||
|
final active = _showingB ? _compareB! : _compareA!;
|
||||||
|
_statistics = _statisticsService.calculateStatistics(
|
||||||
|
[active],
|
||||||
|
period: StatsPeriod.all,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Filtrer les sessions avant calcul
|
// Filtrer les sessions avant calcul
|
||||||
final filteredSessions = _allSessions.where((s) {
|
final filteredSessions = _allSessions.where((s) {
|
||||||
final weaponMatch = _selectedWeapon == 'Toutes' || s.weapon == _selectedWeapon;
|
final weaponMatch = _selectedWeapon == 'Toutes' || s.weapon == _selectedWeapon;
|
||||||
@@ -93,6 +112,80 @@ class _StatisticsScreenState extends State<StatisticsScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _sessionLabel(Session s) {
|
||||||
|
final d = s.createdAt;
|
||||||
|
final date = '${d.day.toString().padLeft(2, '0')}/${d.month.toString().padLeft(2, '0')}/${d.year}';
|
||||||
|
return '${s.weapon} • $date • ${s.totalScore} pts';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sélection des 2 sessions à comparer via un dialog à deux listes déroulantes.
|
||||||
|
Future<void> _openCompareDialog() async {
|
||||||
|
if (_allSessions.length < 2) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Il faut au moins 2 sessions enregistrées pour comparer.')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Session? a = _compareA ?? _allSessions[0];
|
||||||
|
Session? b = _compareB ?? _allSessions[1];
|
||||||
|
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => StatefulBuilder(
|
||||||
|
builder: (context, setState) => AlertDialog(
|
||||||
|
title: const Text('Comparer 2 sessions'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
DropdownButtonFormField<Session>(
|
||||||
|
initialValue: a,
|
||||||
|
isExpanded: true,
|
||||||
|
decoration: const InputDecoration(labelText: 'Session A'),
|
||||||
|
items: _allSessions
|
||||||
|
.map((s) => DropdownMenuItem(value: s, child: Text(_sessionLabel(s), overflow: TextOverflow.ellipsis)))
|
||||||
|
.toList(),
|
||||||
|
onChanged: (v) => setState(() => a = v),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
DropdownButtonFormField<Session>(
|
||||||
|
initialValue: b,
|
||||||
|
isExpanded: true,
|
||||||
|
decoration: const InputDecoration(labelText: 'Session B'),
|
||||||
|
items: _allSessions
|
||||||
|
.map((s) => DropdownMenuItem(value: s, child: Text(_sessionLabel(s), overflow: TextOverflow.ellipsis)))
|
||||||
|
.toList(),
|
||||||
|
onChanged: (v) => setState(() => b = v),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Annuler')),
|
||||||
|
ElevatedButton(onPressed: () => Navigator.pop(context, true), child: const Text('Comparer')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed == true && a != null && b != null) {
|
||||||
|
setState(() {
|
||||||
|
_compareA = a;
|
||||||
|
_compareB = b;
|
||||||
|
_showingB = false;
|
||||||
|
_calculateStats();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _exitCompareMode() {
|
||||||
|
setState(() {
|
||||||
|
_compareA = null;
|
||||||
|
_compareB = null;
|
||||||
|
_showingB = false;
|
||||||
|
_calculateStats();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
List<double> _getScoreHistory() {
|
List<double> _getScoreHistory() {
|
||||||
if (_statistics == null || _statistics!.sessions.isEmpty) return [0];
|
if (_statistics == null || _statistics!.sessions.isEmpty) return [0];
|
||||||
// Sort sessions by date and take last 10
|
// Sort sessions by date and take last 10
|
||||||
@@ -163,6 +256,10 @@ class _StatisticsScreenState extends State<StatisticsScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 1bis. COMPARATEUR DE SESSIONS
|
||||||
|
_buildComparator(),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// 2. DONNÉES RAPIDES (Tirs et Sessions)
|
// 2. DONNÉES RAPIDES (Tirs et Sessions)
|
||||||
@@ -257,6 +354,94 @@ class _StatisticsScreenState extends State<StatisticsScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Comparateur : bouton pour choisir 2 sessions, puis switch pour alterner.
|
||||||
|
Widget _buildComparator() {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
if (!_compareMode) {
|
||||||
|
return SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
onPressed: _openCompareDialog,
|
||||||
|
icon: const Icon(Icons.compare_arrows),
|
||||||
|
label: const Text('Comparer 2 sessions'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final activeColor = const Color(0xFF1A73E8);
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: activeColor.withValues(alpha: 0.08),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: activeColor.withValues(alpha: 0.3)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.compare_arrows, color: activeColor, size: 18),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text('Comparaison', style: TextStyle(fontWeight: FontWeight.bold, color: activeColor)),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
tooltip: 'Quitter la comparaison',
|
||||||
|
icon: const Icon(Icons.close, size: 18),
|
||||||
|
onPressed: _exitCompareMode,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
// Switch pour alterner entre les 2 sessions.
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'A · ${_sessionLabel(_compareA!)}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: _showingB ? theme.textTheme.bodySmall?.color?.withValues(alpha: 0.5) : null,
|
||||||
|
fontWeight: _showingB ? FontWeight.normal : FontWeight.bold,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Switch(
|
||||||
|
value: _showingB,
|
||||||
|
activeThumbColor: activeColor,
|
||||||
|
onChanged: (v) => setState(() {
|
||||||
|
_showingB = v;
|
||||||
|
_calculateStats();
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'B · ${_sessionLabel(_compareB!)}',
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: _showingB ? null : theme.textTheme.bodySmall?.color?.withValues(alpha: 0.5),
|
||||||
|
fontWeight: _showingB ? FontWeight.bold : FontWeight.normal,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Affichage : session ${_showingB ? 'B' : 'A'}',
|
||||||
|
style: TextStyle(fontSize: 11, color: theme.textTheme.bodySmall?.color?.withValues(alpha: 0.6)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildDropdown(
|
Widget _buildDropdown(
|
||||||
String label,
|
String label,
|
||||||
String value,
|
String value,
|
||||||
|
|||||||
Reference in New Issue
Block a user