feat: implement base architecture and core repositories for weapon tracking and target analysis functionality
This commit is contained in:
@@ -1,10 +1,3 @@
|
||||
/// Écran de détail d'une session.
|
||||
///
|
||||
/// Affiche la visualisation complète d'une session sauvegardée :
|
||||
/// image de la cible avec overlay des impacts, scores recalculés,
|
||||
/// statistiques de groupement et lien vers les statistiques détaillées.
|
||||
library;
|
||||
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -12,6 +5,8 @@ import 'package:intl/intl.dart';
|
||||
import '../../core/constants/app_constants.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../data/models/session.dart';
|
||||
import '../../data/models/target_analysis.dart';
|
||||
import '../../data/models/target_type.dart';
|
||||
import '../../data/repositories/session_repository.dart';
|
||||
import '../../services/score_calculator_service.dart';
|
||||
import '../../services/grouping_analyzer_service.dart';
|
||||
@@ -20,7 +15,7 @@ import '../analysis/widgets/score_card.dart';
|
||||
import '../analysis/widgets/grouping_stats.dart';
|
||||
import '../statistics/statistics_screen.dart';
|
||||
|
||||
class SessionDetailScreen extends StatelessWidget {
|
||||
class SessionDetailScreen extends StatefulWidget {
|
||||
final Session session;
|
||||
|
||||
const SessionDetailScreen({
|
||||
@@ -28,33 +23,40 @@ class SessionDetailScreen extends StatelessWidget {
|
||||
required this.session,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SessionDetailScreen> createState() => _SessionDetailScreenState();
|
||||
}
|
||||
|
||||
class _SessionDetailScreenState extends State<SessionDetailScreen> {
|
||||
int _currentTargetIndex = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scoreCalculator = context.read<ScoreCalculatorService>();
|
||||
final groupingAnalyzer = context.read<GroupingAnalyzerService>();
|
||||
|
||||
final currentAnalysis = widget.session.analyses[_currentTargetIndex];
|
||||
|
||||
final scoreResult = scoreCalculator.calculateScores(
|
||||
shots: session.shots,
|
||||
targetType: session.targetType,
|
||||
targetCenterX: session.targetCenterX ?? 0.5,
|
||||
targetCenterY: session.targetCenterY ?? 0.5,
|
||||
targetRadius: session.targetRadius ?? 0.4,
|
||||
shots: currentAnalysis.shots,
|
||||
targetType: currentAnalysis.targetType,
|
||||
targetCenterX: currentAnalysis.targetCenterX ?? 0.5,
|
||||
targetCenterY: currentAnalysis.targetCenterY ?? 0.5,
|
||||
targetRadius: currentAnalysis.targetRadius ?? 0.4,
|
||||
);
|
||||
|
||||
final groupingResult = groupingAnalyzer.analyzeGrouping(session.shots);
|
||||
final groupingResult = groupingAnalyzer.analyzeGrouping(currentAnalysis.shots);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
DateFormat('dd/MM/yyyy HH:mm').format(session.createdAt),
|
||||
),
|
||||
title: Text(widget.session.weapon),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.analytics),
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => StatisticsScreen(singleSession: session),
|
||||
builder: (_) => StatisticsScreen(singleSession: widget.session),
|
||||
),
|
||||
),
|
||||
tooltip: 'Statistiques',
|
||||
@@ -70,15 +72,22 @@ class SessionDetailScreen extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Weapon and session overview
|
||||
_buildSessionHeader(context),
|
||||
|
||||
// Target selector if multiple targets
|
||||
if (widget.session.targetCount > 1)
|
||||
_buildTargetSelector(),
|
||||
|
||||
// Target image with overlay
|
||||
AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (File(session.imagePath).existsSync())
|
||||
if (File(currentAnalysis.imagePath).existsSync())
|
||||
Image.file(
|
||||
File(session.imagePath),
|
||||
File(currentAnalysis.imagePath),
|
||||
fit: BoxFit.contain,
|
||||
)
|
||||
else
|
||||
@@ -89,14 +98,14 @@ class SessionDetailScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
TargetOverlay(
|
||||
shots: session.shots,
|
||||
targetCenterX: session.targetCenterX ?? 0.5,
|
||||
targetCenterY: session.targetCenterY ?? 0.5,
|
||||
targetRadius: session.targetRadius ?? 0.4,
|
||||
targetType: session.targetType,
|
||||
groupingCenterX: session.groupingCenterX,
|
||||
groupingCenterY: session.groupingCenterY,
|
||||
groupingDiameter: session.groupingDiameter,
|
||||
shots: currentAnalysis.shots,
|
||||
targetCenterX: currentAnalysis.targetCenterX ?? 0.5,
|
||||
targetCenterY: currentAnalysis.targetCenterY ?? 0.5,
|
||||
targetRadius: currentAnalysis.targetRadius ?? 0.4,
|
||||
targetType: currentAnalysis.targetType,
|
||||
groupingCenterX: currentAnalysis.groupingCenterX,
|
||||
groupingCenterY: currentAnalysis.groupingCenterY,
|
||||
groupingDiameter: currentAnalysis.groupingDiameter,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -107,31 +116,37 @@ class SessionDetailScreen extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Session info
|
||||
_buildSessionInfo(context),
|
||||
// Analysis Info
|
||||
_buildAnalysisInfo(context, currentAnalysis),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Score card
|
||||
// Score card for current target
|
||||
ScoreCard(
|
||||
totalScore: session.totalScore,
|
||||
shotCount: session.shotCount,
|
||||
totalScore: currentAnalysis.totalScore,
|
||||
shotCount: currentAnalysis.shotCount,
|
||||
scoreResult: scoreResult,
|
||||
targetType: session.targetType,
|
||||
targetType: currentAnalysis.targetType,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Grouping stats
|
||||
if (session.shotCount > 1)
|
||||
// Grouping stats for current target
|
||||
if (currentAnalysis.shotCount > 1)
|
||||
GroupingStats(
|
||||
groupingResult: groupingResult,
|
||||
targetCenterX: session.targetCenterX ?? 0.5,
|
||||
targetCenterY: session.targetCenterY ?? 0.5,
|
||||
targetCenterX: currentAnalysis.targetCenterX ?? 0.5,
|
||||
targetCenterY: currentAnalysis.targetCenterY ?? 0.5,
|
||||
),
|
||||
|
||||
// Notes
|
||||
if (session.notes != null && session.notes!.isNotEmpty) ...[
|
||||
// Notes for current target
|
||||
if (currentAnalysis.notes != null && currentAnalysis.notes!.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
_buildNotesCard(context),
|
||||
_buildNotesCard(context, currentAnalysis.notes!),
|
||||
],
|
||||
|
||||
// Session notes
|
||||
if (widget.session.notes != null && widget.session.notes!.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
_buildNotesCard(context, widget.session.notes!, title: 'Notes de session'),
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -142,16 +157,83 @@ class SessionDetailScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSessionInfo(BuildContext context) {
|
||||
Widget _buildSessionHeader(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
color: AppTheme.primaryColor.withValues(alpha: 0.1),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
DateFormat('EEEE dd MMMM yyyy', 'fr_FR').format(widget.session.createdAt),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
DateFormat('HH:mm').format(widget.session.createdAt),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'Score Total: ${widget.session.totalScore}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, color: AppTheme.primaryColor),
|
||||
),
|
||||
Text(
|
||||
'${widget.session.totalShots} tirs au total',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTargetSelector() {
|
||||
return Container(
|
||||
height: 50,
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: widget.session.targetCount,
|
||||
itemBuilder: (context, index) {
|
||||
final isSelected = _currentTargetIndex == index;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: ChoiceChip(
|
||||
label: Text('Cible ${index + 1}'),
|
||||
selected: isSelected,
|
||||
onSelected: (selected) {
|
||||
if (selected) {
|
||||
setState(() => _currentTargetIndex = index);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAnalysisInfo(BuildContext context, TargetAnalysis analysis) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(AppConstants.defaultPadding),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
session.targetType == session.targetType
|
||||
? Icons.track_changes
|
||||
: Icons.person,
|
||||
analysis.targetType == TargetType.concentric ? Icons.track_changes : Icons.person,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
@@ -160,14 +242,11 @@ class SessionDetailScreen extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
session.targetType.displayName,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
analysis.targetType.displayName,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
DateFormat('EEEE dd MMMM yyyy, HH:mm', 'fr_FR')
|
||||
.format(session.createdAt),
|
||||
'Cible ${_currentTargetIndex + 1} sur ${widget.session.targetCount}',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
@@ -179,7 +258,7 @@ class SessionDetailScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNotesCard(BuildContext context) {
|
||||
Widget _buildNotesCard(BuildContext context, String notes, {String title = 'Notes'}) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(AppConstants.defaultPadding),
|
||||
@@ -188,18 +267,16 @@ class SessionDetailScreen extends StatelessWidget {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.notes, color: AppTheme.primaryColor),
|
||||
const Icon(Icons.notes, color: AppTheme.primaryColor, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Notes',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
title,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
Text(session.notes!),
|
||||
Text(notes),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -211,7 +288,7 @@ class SessionDetailScreen extends StatelessWidget {
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Supprimer'),
|
||||
content: const Text('Voulez-vous vraiment supprimer cette session?'),
|
||||
content: const Text('Voulez-vous vraiment supprimer cette session entière ?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
@@ -231,20 +308,17 @@ class SessionDetailScreen extends StatelessWidget {
|
||||
if (confirmed == true && context.mounted) {
|
||||
try {
|
||||
final repository = context.read<SessionRepository>();
|
||||
await repository.deleteSession(session.id);
|
||||
await repository.deleteSession(widget.session.id);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Session supprimee')),
|
||||
const SnackBar(content: Text('Session supprimée')),
|
||||
);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Erreur: $e'),
|
||||
backgroundColor: AppTheme.errorColor,
|
||||
),
|
||||
SnackBar(content: Text('Erreur: $e'), backgroundColor: AppTheme.errorColor),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user