454 lines
15 KiB
Dart
454 lines
15 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import '../../core/constants/app_constants.dart';
|
|
import '../../core/theme/app_theme.dart';
|
|
import '../../data/repositories/session_repository.dart';
|
|
import '../capture/capture_screen.dart';
|
|
import '../history/history_screen.dart';
|
|
import '../statistics/statistics_screen.dart';
|
|
import 'package:fl_chart/fl_chart.dart';
|
|
import '../../data/models/session.dart';
|
|
import '../settings/settings_screen.dart';
|
|
import '../session/session_setup_screen.dart';
|
|
import '../session/session_provider.dart';
|
|
import 'widgets/stats_card.dart';
|
|
|
|
class HomeScreen extends StatefulWidget {
|
|
const HomeScreen({super.key});
|
|
|
|
@override
|
|
State<HomeScreen> createState() => _HomeScreenState();
|
|
}
|
|
|
|
class _HomeScreenState extends State<HomeScreen> {
|
|
Map<String, dynamic>? _stats;
|
|
List<Session> _recentSessions = [];
|
|
bool _isLoading = true;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadStats();
|
|
}
|
|
|
|
Future<void> _loadStats() async {
|
|
final repository = context.read<SessionRepository>();
|
|
final stats = await repository.getStatistics();
|
|
final sessions = await repository.getAllSessions();
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_stats = stats;
|
|
_recentSessions = sessions;
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
leading: const Center(
|
|
child: Text(
|
|
'v1.0.2',
|
|
style: TextStyle(fontSize: 12, color: Colors.white70),
|
|
),
|
|
),
|
|
title: const Text('Bully'),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.settings),
|
|
onPressed: () => _navigateToSettings(context),
|
|
tooltip: 'Paramètres',
|
|
),
|
|
],
|
|
),
|
|
body: RefreshIndicator(
|
|
onRefresh: _loadStats,
|
|
child: SingleChildScrollView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
padding: const EdgeInsets.all(AppConstants.defaultPadding),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
_buildHeader(),
|
|
const SizedBox(height: AppConstants.largePadding),
|
|
_buildMainActionButton(context),
|
|
const SizedBox(height: AppConstants.largePadding),
|
|
if (_isLoading)
|
|
const Center(child: CircularProgressIndicator())
|
|
else if (_stats != null)
|
|
_buildStatsSection(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHeader() {
|
|
return Column(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(24),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.primaryColor.withValues(alpha: 0.1),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(
|
|
Icons.track_changes,
|
|
size: 64,
|
|
color: AppTheme.primaryColor,
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Analyse de Cibles',
|
|
style: Theme.of(
|
|
context,
|
|
).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Scannez vos cibles et analysez vos performances',
|
|
style: Theme.of(
|
|
context,
|
|
).textTheme.bodyLarge?.copyWith(color: AppTheme.textSecondary),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildMainActionButton(BuildContext context) {
|
|
final sessionProvider = context.watch<SessionProvider>();
|
|
final isSessionActive = sessionProvider.isSessionActive;
|
|
|
|
// Si aucune session n'est active, on affiche juste le bouton de départ habituel
|
|
if (!isSessionActive) {
|
|
return ElevatedButton.icon(
|
|
onPressed: () => _navigateToSessionSetup(context),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppTheme.primaryColor,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(vertical: 20),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
|
|
),
|
|
),
|
|
icon: const Icon(Icons.add_circle_outline, size: 28),
|
|
label: const Text(
|
|
'Démarrer la session',
|
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
),
|
|
);
|
|
}
|
|
|
|
// MODIFICATION : Alignement sur une seule ligne (80% / 20%) avec une croix pour l'annulation
|
|
return Row(
|
|
children: [
|
|
// 80% de largeur pour continuer la session
|
|
Expanded(
|
|
flex: 8,
|
|
child: ElevatedButton.icon(
|
|
onPressed: () => _navigateToSessionSetup(context),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppTheme.secondaryColor,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(vertical: 20),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
|
|
),
|
|
),
|
|
icon: const Icon(Icons.play_circle_outline, size: 28),
|
|
label: const Text(
|
|
'Continuer la session',
|
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
// 20% de largeur pour terminer/annuler la session (Bouton Croix)
|
|
Expanded(
|
|
flex: 2,
|
|
child: OutlinedButton(
|
|
onPressed: () {
|
|
// Boite de dialogue de confirmation pour éviter les erreurs
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: const Text('Terminer la session ?'),
|
|
content: const Text('Êtes-vous sûr de vouloir clôturer la session en cours ?'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text('ANNULER'),
|
|
),
|
|
TextButton(
|
|
onPressed: () {
|
|
Navigator.pop(context);
|
|
// Clôture propre de la session dans le State global
|
|
context.read<SessionProvider>().endSession();
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Session terminée avec succès !'),
|
|
backgroundColor: AppTheme.successColor,
|
|
),
|
|
);
|
|
},
|
|
child: const Text('TERMINER', style: TextStyle(color: Colors.redAccent)),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
},
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: Colors.redAccent,
|
|
side: const BorderSide(color: Colors.redAccent, width: 1.5),
|
|
padding: const EdgeInsets.symmetric(vertical: 20), // Même hauteur que le bouton continuer
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
|
|
),
|
|
),
|
|
child: const Icon(Icons.close, size: 28),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildStatsSection() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Statistiques',
|
|
style: Theme.of(
|
|
context,
|
|
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: InkWell(
|
|
onTap: () => _navigateToStatistics(context),
|
|
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
|
|
child: StatsCard(
|
|
icon: Icons.assessment,
|
|
title: 'Sessions',
|
|
value: '${_stats!['totalSessions']}',
|
|
color: AppTheme.primaryColor,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: StatsCard(
|
|
icon: Icons.gps_fixed,
|
|
title: 'Tirs',
|
|
value: '${_stats!['totalShots']}',
|
|
color: AppTheme.secondaryColor,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
|
child: Container(
|
|
height: 150,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).cardColor,
|
|
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: _recentSessions.length < 2
|
|
? const Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.show_chart, size: 40, color: Colors.grey),
|
|
Text(
|
|
'Pas assez de données',
|
|
style: TextStyle(color: Colors.grey, fontSize: 12),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
: _buildHomeTrendChart(),
|
|
),
|
|
),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: InkWell(
|
|
onTap: () => _navigateToHistory(context),
|
|
borderRadius: BorderRadius.circular(AppConstants.borderRadius),
|
|
child: StatsCard(
|
|
icon: Icons.trending_up,
|
|
title: 'Historique',
|
|
value: (_stats!['averageScore'] as double).toStringAsFixed(1),
|
|
color: AppTheme.warningColor,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: StatsCard(
|
|
icon: Icons.emoji_events,
|
|
title: 'Meilleur',
|
|
value: '${_stats!['bestScore']}',
|
|
color: AppTheme.successColor,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
void _navigateToSessionSetup(BuildContext context) async {
|
|
final sessionProvider = context.read<SessionProvider>();
|
|
if (sessionProvider.isSessionActive) {
|
|
await Navigator.push(
|
|
context,
|
|
MaterialPageRoute(builder: (_) => const CaptureScreen()),
|
|
);
|
|
} else {
|
|
await Navigator.push(
|
|
context,
|
|
MaterialPageRoute(builder: (_) => const SessionSetupScreen()),
|
|
);
|
|
}
|
|
_loadStats();
|
|
}
|
|
|
|
void _navigateToHistory(BuildContext context) async {
|
|
await Navigator.push(
|
|
context,
|
|
MaterialPageRoute(builder: (_) => const HistoryScreen()),
|
|
);
|
|
_loadStats();
|
|
}
|
|
|
|
void _navigateToStatistics(BuildContext context) async {
|
|
await Navigator.push(
|
|
context,
|
|
MaterialPageRoute(builder: (_) => const StatisticsScreen()),
|
|
);
|
|
_loadStats();
|
|
}
|
|
|
|
Widget _buildHomeTrendChart() {
|
|
final sorted = List<Session>.from(_recentSessions)
|
|
..sort((a, b) => a.createdAt.compareTo(b.createdAt));
|
|
final display =
|
|
sorted.length > 7 ? sorted.sublist(sorted.length - 7) : sorted;
|
|
|
|
return LineChart(
|
|
LineChartData(
|
|
gridData: FlGridData(
|
|
show: true,
|
|
drawVerticalLine: false,
|
|
getDrawingHorizontalLine: (value) => FlLine(
|
|
color: Colors.grey.withValues(alpha: 0.1),
|
|
strokeWidth: 1,
|
|
),
|
|
),
|
|
titlesData: FlTitlesData(
|
|
show: true,
|
|
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
|
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
|
bottomTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
reservedSize: 22,
|
|
interval: 1,
|
|
getTitlesWidget: (value, meta) {
|
|
final index = value.toInt();
|
|
if (index < 0 || index >= display.length) return const SizedBox();
|
|
final date = display[index].createdAt;
|
|
return Padding(
|
|
padding: const EdgeInsets.only(top: 4.0),
|
|
child: Text(
|
|
'${date.day}/${date.month}',
|
|
style: TextStyle(
|
|
fontSize: 9,
|
|
color: Colors.grey.withValues(alpha: 0.8),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
leftTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
reservedSize: 28,
|
|
getTitlesWidget: (value, meta) {
|
|
return Text(
|
|
value.toInt().toString(),
|
|
style: TextStyle(
|
|
fontSize: 9,
|
|
color: Colors.grey.withValues(alpha: 0.8),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
borderData: FlBorderData(
|
|
show: true,
|
|
border: Border(
|
|
bottom: BorderSide(color: Colors.grey.withValues(alpha: 0.2)),
|
|
left: BorderSide(color: Colors.grey.withValues(alpha: 0.2)),
|
|
),
|
|
),
|
|
lineBarsData: [
|
|
LineChartBarData(
|
|
spots: display.asMap().entries.map((e) {
|
|
return FlSpot(e.key.toDouble(), e.value.averageScore);
|
|
}).toList(),
|
|
isCurved: true,
|
|
color: AppTheme.primaryColor,
|
|
barWidth: 3,
|
|
isStrokeCapRound: true,
|
|
dotData: FlDotData(
|
|
show: true,
|
|
getDotPainter: (spot, percent, barData, index) => FlDotCirclePainter(
|
|
radius: 3,
|
|
color: Colors.white,
|
|
strokeWidth: 2,
|
|
strokeColor: AppTheme.primaryColor,
|
|
),
|
|
),
|
|
belowBarData: BarAreaData(
|
|
show: true,
|
|
color: AppTheme.primaryColor.withValues(alpha: 0.1),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _navigateToSettings(BuildContext context) async {
|
|
await Navigator.push(
|
|
context,
|
|
MaterialPageRoute(builder: (_) => const SettingsScreen()),
|
|
);
|
|
_loadStats();
|
|
}
|
|
} |