990 lines
33 KiB
Dart
990 lines
33 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:fl_chart/fl_chart.dart';
|
|
import 'package:intl/intl.dart';
|
|
import '../../main_navigation_holder.dart';
|
|
import '../../core/theme/app_theme.dart';
|
|
import '../../core/widgets/glass_container.dart';
|
|
import '../../data/models/session.dart';
|
|
import '../../data/repositories/session_repository.dart';
|
|
import '../capture/capture_screen.dart';
|
|
import '../history/history_screen.dart';
|
|
import '../history/session_detail_screen.dart';
|
|
import '../session/session_setup_screen.dart';
|
|
import '../session/session_provider.dart';
|
|
import '../settings/settings_screen.dart';
|
|
import '../statistics/statistics_screen.dart';
|
|
import 'widgets/stats_card.dart';
|
|
|
|
class HomeScreen extends StatefulWidget {
|
|
final int refreshTick;
|
|
|
|
const HomeScreen({super.key, this.refreshTick = 0});
|
|
|
|
@override
|
|
State<HomeScreen> createState() => _HomeScreenState();
|
|
}
|
|
|
|
class _HomeScreenState extends State<HomeScreen> {
|
|
Map<String, dynamic>? _stats;
|
|
List<Session> _recentSessions = [];
|
|
bool _isLoading = true;
|
|
|
|
SessionProvider? _sessionProvider;
|
|
bool _wasSessionActive = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadStats();
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(HomeScreen oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.refreshTick != widget.refreshTick) {
|
|
_loadStats();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
final provider = context.read<SessionProvider>();
|
|
if (provider != _sessionProvider) {
|
|
_sessionProvider?.removeListener(_onSessionChanged);
|
|
_sessionProvider = provider;
|
|
_sessionProvider!.addListener(_onSessionChanged);
|
|
_wasSessionActive = provider.isSessionActive;
|
|
}
|
|
}
|
|
|
|
void _onSessionChanged() {
|
|
final isActive = _sessionProvider?.isSessionActive ?? false;
|
|
if (_wasSessionActive && !isActive) {
|
|
_loadStats();
|
|
}
|
|
_wasSessionActive = isActive;
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_sessionProvider?.removeListener(_onSessionChanged);
|
|
super.dispose();
|
|
}
|
|
|
|
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) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
final primaryColor = Theme.of(context).colorScheme.primary;
|
|
final textSecondary = isDark ? AppTheme.darkTextSecondary : AppTheme.lightTextSecondary;
|
|
|
|
return Scaffold(
|
|
extendBody: true,
|
|
appBar: AppBar(
|
|
titleSpacing: 16,
|
|
leading: Padding(
|
|
padding: const EdgeInsets.only(left: 16),
|
|
child: Center(
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: (isDark ? Colors.white : Colors.black).withValues(alpha: 0.06),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(
|
|
color: isDark ? AppTheme.darkBorder : AppTheme.lightBorder,
|
|
),
|
|
),
|
|
child: Text(
|
|
'v1.0.3',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
color: textSecondary,
|
|
letterSpacing: 0.5,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
leadingWidth: 72,
|
|
title: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(6),
|
|
decoration: BoxDecoration(
|
|
color: primaryColor.withValues(alpha: 0.18),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(Icons.gps_fixed, color: primaryColor, size: 18),
|
|
),
|
|
const SizedBox(width: 10),
|
|
const Text(
|
|
'BULLY',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w900,
|
|
letterSpacing: 2.0,
|
|
fontSize: 20,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.settings_outlined),
|
|
onPressed: () => _navigateToSettings(context),
|
|
tooltip: 'Paramètres',
|
|
),
|
|
const SizedBox(width: 8),
|
|
],
|
|
),
|
|
body: Stack(
|
|
children: [
|
|
// Ambiance lumineuse d'arrière-plan (lueurs dégradées)
|
|
if (isDark) ...[
|
|
Positioned(
|
|
top: -60,
|
|
right: -60,
|
|
child: Container(
|
|
width: 260,
|
|
height: 260,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: RadialGradient(
|
|
colors: [
|
|
primaryColor.withValues(alpha: 0.18),
|
|
Colors.transparent,
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
top: 300,
|
|
left: -80,
|
|
child: Container(
|
|
width: 220,
|
|
height: 220,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: RadialGradient(
|
|
colors: [
|
|
AppTheme.secondaryColor.withValues(alpha: 0.09),
|
|
Colors.transparent,
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
|
|
RefreshIndicator(
|
|
onRefresh: _loadStats,
|
|
color: primaryColor,
|
|
child: SingleChildScrollView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
// Padding bas important pour laisser respirer le dock flottant
|
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 100),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
_buildHeader(isDark, primaryColor),
|
|
const SizedBox(height: 16),
|
|
_buildMainActionSection(context, isDark, primaryColor),
|
|
const SizedBox(height: 22),
|
|
if (_isLoading)
|
|
const Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 40),
|
|
child: Center(child: CircularProgressIndicator()),
|
|
)
|
|
else if (_stats != null) ...[
|
|
_buildStatsSection(isDark, primaryColor),
|
|
const SizedBox(height: 22),
|
|
_buildRecentSessionsSection(isDark, primaryColor),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHeader(bool isDark, Color primaryColor) {
|
|
return GlassContainer(
|
|
borderRadius: 20,
|
|
blur: 16,
|
|
glowColor: primaryColor,
|
|
borderColor: isDark
|
|
? primaryColor.withValues(alpha: 0.25)
|
|
: primaryColor.withValues(alpha: 0.15),
|
|
padding: const EdgeInsets.all(18),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
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,
|
|
),
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: primaryColor.withValues(alpha: 0.35),
|
|
blurRadius: 12,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: const Icon(
|
|
Icons.track_changes,
|
|
size: 28,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: primaryColor.withValues(alpha: isDark ? 0.25 : 0.15),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Text(
|
|
'BALISTIQUE & CIBLES',
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w800,
|
|
letterSpacing: 1.2,
|
|
color: primaryColor,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 6),
|
|
Container(
|
|
width: 6,
|
|
height: 6,
|
|
decoration: const BoxDecoration(
|
|
color: AppTheme.secondaryColor,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'Télémétrie de Tir',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w800,
|
|
color: isDark ? AppTheme.darkTextPrimary : AppTheme.lightTextPrimary,
|
|
letterSpacing: -0.3,
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
'Analysez vos groupements et scores',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: isDark ? AppTheme.darkTextSecondary : AppTheme.lightTextSecondary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildMainActionSection(BuildContext context, bool isDark, Color primaryColor) {
|
|
final sessionProvider = context.watch<SessionProvider>();
|
|
final isSessionActive = sessionProvider.isSessionActive;
|
|
|
|
if (!isSessionActive) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(20),
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
HSLColor.fromColor(primaryColor).withLightness((HSLColor.fromColor(primaryColor).lightness - 0.1).clamp(0.0, 1.0)).toColor(),
|
|
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: isDark ? 0.45 : 0.3),
|
|
blurRadius: 20,
|
|
spreadRadius: -2,
|
|
offset: const Offset(0, 8),
|
|
),
|
|
],
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(20),
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
onTap: () => _navigateToSessionSetup(context),
|
|
splashColor: Colors.white24,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 20),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(6),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withValues(alpha: 0.25),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(Icons.add, color: Colors.white, size: 22),
|
|
),
|
|
const SizedBox(width: 14),
|
|
const Text(
|
|
'NOUVELLE SESSION DE TIR',
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w900,
|
|
color: Colors.white,
|
|
letterSpacing: 1.0,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Session en cours : Glass Card dynamique avec halo vert
|
|
return GlassContainer(
|
|
borderRadius: 20,
|
|
blur: 16,
|
|
glowColor: AppTheme.secondaryColor,
|
|
borderColor: AppTheme.secondaryColor.withValues(alpha: isDark ? 0.45 : 0.35),
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.secondaryColor.withValues(alpha: isDark ? 0.22 : 0.15),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(
|
|
color: AppTheme.secondaryColor.withValues(alpha: 0.4),
|
|
),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 8,
|
|
height: 8,
|
|
decoration: const BoxDecoration(
|
|
color: AppTheme.secondaryColor,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
const SizedBox(width: 6),
|
|
const Text(
|
|
'SESSION EN COURS',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w800,
|
|
color: AppTheme.secondaryColor,
|
|
letterSpacing: 0.8,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const Spacer(),
|
|
if (sessionProvider.currentWeapon != null)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: (isDark ? Colors.white : Colors.black).withValues(alpha: 0.06),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Text(
|
|
sessionProvider.currentWeapon!,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w700,
|
|
color: isDark ? AppTheme.darkTextPrimary : AppTheme.lightTextPrimary,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 14),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
flex: 4,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(14),
|
|
gradient: const LinearGradient(
|
|
colors: [Color(0xFF10B981), Color(0xFF059669)],
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: AppTheme.secondaryColor.withValues(alpha: 0.3),
|
|
blurRadius: 12,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: ElevatedButton.icon(
|
|
onPressed: () => _navigateToSessionSetup(context),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.transparent,
|
|
foregroundColor: Colors.white,
|
|
shadowColor: Colors.transparent,
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
),
|
|
icon: const Icon(Icons.play_arrow_rounded, size: 24),
|
|
label: const Text(
|
|
'Reprendre la session',
|
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w800),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
flex: 1,
|
|
child: OutlinedButton(
|
|
onPressed: () => _confirmEndSession(context),
|
|
style: OutlinedButton.styleFrom(
|
|
foregroundColor: AppTheme.errorColor,
|
|
side: BorderSide(
|
|
color: AppTheme.errorColor.withValues(alpha: 0.7),
|
|
width: 1.4,
|
|
),
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
),
|
|
child: const Icon(Icons.close, size: 20),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _confirmEndSession(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext dialogContext) {
|
|
return AlertDialog(
|
|
title: const Text('Clôturer la session ?'),
|
|
content: const Text(
|
|
'Êtes-vous sûr de vouloir terminer la session en cours ? Vos tirs actuels seront enregistrés.',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(dialogContext),
|
|
child: const Text('Annuler'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
Navigator.pop(dialogContext);
|
|
context.read<SessionProvider>().endSession();
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Session terminée avec succès'),
|
|
backgroundColor: AppTheme.successColor,
|
|
),
|
|
);
|
|
openMainTab(mainTabStats);
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppTheme.errorColor,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
child: const Text('Terminer'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildStatsSection(bool isDark, Color primaryColor) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'Télémétrie Globale',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w800,
|
|
color: isDark ? AppTheme.darkTextPrimary : AppTheme.lightTextPrimary,
|
|
letterSpacing: 0.2,
|
|
),
|
|
),
|
|
InkWell(
|
|
onTap: () => _navigateToStatistics(context),
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
child: Row(
|
|
children: [
|
|
Text(
|
|
'Détails',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w700,
|
|
color: primaryColor,
|
|
),
|
|
),
|
|
Icon(Icons.chevron_right, size: 18, color: primaryColor),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: InkWell(
|
|
onTap: () => _navigateToStatistics(context),
|
|
borderRadius: BorderRadius.circular(18),
|
|
child: StatsCard(
|
|
icon: Icons.assignment_outlined,
|
|
title: 'Sessions',
|
|
value: '${_stats!['totalSessions']}',
|
|
color: primaryColor,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: StatsCard(
|
|
icon: Icons.track_changes,
|
|
title: 'Tirs analysés',
|
|
value: '${_stats!['totalShots']}',
|
|
color: AppTheme.secondaryColor,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: StatsCard(
|
|
icon: Icons.pie_chart_outline,
|
|
title: 'Score moyen',
|
|
value: '${_stats!['averageScore'].toStringAsFixed(1)} pts',
|
|
subtitle: 'sur 10 max',
|
|
color: AppTheme.accentGold,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: StatsCard(
|
|
icon: Icons.star_border,
|
|
title: 'Meilleur score',
|
|
value: '${_stats!['bestScore']} pts',
|
|
color: AppTheme.secondaryColor,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 14),
|
|
GlassContainer(
|
|
borderRadius: 20,
|
|
blur: 16,
|
|
borderColor: isDark ? Colors.white.withValues(alpha: 0.1) : Colors.black.withValues(alpha: 0.06),
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'Progression récente',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w700,
|
|
color: isDark ? AppTheme.darkTextPrimary : AppTheme.lightTextPrimary,
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: (isDark ? Colors.white : Colors.black).withValues(alpha: 0.05),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Text(
|
|
'7 dernières séances',
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w600,
|
|
color: isDark ? AppTheme.darkTextMuted : AppTheme.lightTextMuted,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
SizedBox(
|
|
height: 120,
|
|
child: _recentSessions.length < 2
|
|
? Center(
|
|
child: Text(
|
|
'2 séances minimum pour afficher la courbe',
|
|
style: TextStyle(
|
|
color: isDark ? AppTheme.darkTextMuted : AppTheme.lightTextMuted,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
)
|
|
: _buildHomeTrendChart(isDark, primaryColor),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildHomeTrendChart(bool isDark, Color primaryColor) {
|
|
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;
|
|
|
|
final gridColor = isDark ? Colors.white.withValues(alpha: 0.06) : Colors.black.withValues(alpha: 0.04);
|
|
final textColor = isDark ? AppTheme.darkTextMuted : AppTheme.lightTextMuted;
|
|
|
|
return LineChart(
|
|
LineChartData(
|
|
gridData: FlGridData(
|
|
show: true,
|
|
drawVerticalLine: false,
|
|
getDrawingHorizontalLine: (value) => FlLine(
|
|
color: gridColor,
|
|
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: 20,
|
|
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: 10, color: textColor),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
leftTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
reservedSize: 24,
|
|
interval: 2,
|
|
getTitlesWidget: (value, meta) {
|
|
if (value < 0 || value > 10) return const SizedBox();
|
|
return Text(
|
|
value.toInt().toString(),
|
|
style: TextStyle(fontSize: 10, color: textColor),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
borderData: FlBorderData(show: false),
|
|
lineBarsData: [
|
|
LineChartBarData(
|
|
spots: display.asMap().entries.map((e) {
|
|
return FlSpot(e.key.toDouble(), e.value.averageScore);
|
|
}).toList(),
|
|
isCurved: true,
|
|
curveSmoothness: 0.35,
|
|
color: primaryColor,
|
|
barWidth: 3,
|
|
isStrokeCapRound: true,
|
|
dotData: FlDotData(
|
|
show: true,
|
|
getDotPainter: (spot, percent, barData, index) => FlDotCirclePainter(
|
|
radius: 3.5,
|
|
color: Colors.white,
|
|
strokeWidth: 2,
|
|
strokeColor: primaryColor,
|
|
),
|
|
),
|
|
belowBarData: BarAreaData(
|
|
show: true,
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
primaryColor.withValues(alpha: isDark ? 0.35 : 0.2),
|
|
primaryColor.withValues(alpha: 0.0),
|
|
],
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildRecentSessionsSection(bool isDark, Color primaryColor) {
|
|
if (_recentSessions.isEmpty) return const SizedBox();
|
|
|
|
final sessionsToShow = _recentSessions.take(3).toList();
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'Dernières Séances',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w800,
|
|
color: isDark ? AppTheme.darkTextPrimary : AppTheme.lightTextPrimary,
|
|
letterSpacing: 0.2,
|
|
),
|
|
),
|
|
InkWell(
|
|
onTap: () => _navigateToHistory(context),
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
|
child: Text(
|
|
'Voir tout (${_recentSessions.length})',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w700,
|
|
color: primaryColor,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
...sessionsToShow.map((session) => _buildRecentSessionTile(session, isDark, primaryColor)),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildRecentSessionTile(Session session, bool isDark, Color primaryColor) {
|
|
final dateFormat = DateFormat('dd MMM yyyy • HH:mm', 'fr_FR');
|
|
final formattedDate = dateFormat.format(session.createdAt);
|
|
|
|
return GlassContainer(
|
|
borderRadius: 16,
|
|
blur: 12,
|
|
margin: const EdgeInsets.only(bottom: 10),
|
|
padding: const EdgeInsets.all(14),
|
|
onTap: () async {
|
|
await Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => SessionDetailScreen(session: session),
|
|
),
|
|
);
|
|
_loadStats();
|
|
},
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 48,
|
|
height: 48,
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
primaryColor.withValues(alpha: 0.25),
|
|
primaryColor.withValues(alpha: 0.08),
|
|
],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(
|
|
color: primaryColor.withValues(alpha: 0.35),
|
|
),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
'${session.totalScore}',
|
|
style: TextStyle(
|
|
fontSize: 17,
|
|
fontWeight: FontWeight.w900,
|
|
color: primaryColor,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
session.weapon,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w700,
|
|
color: isDark ? AppTheme.darkTextPrimary : AppTheme.lightTextPrimary,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
formattedDate,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: isDark ? AppTheme.darkTextSecondary : AppTheme.lightTextSecondary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
decoration: BoxDecoration(
|
|
color: (isDark ? Colors.white : Colors.black).withValues(alpha: 0.06),
|
|
borderRadius: BorderRadius.circular(6),
|
|
border: Border.all(
|
|
color: isDark ? AppTheme.darkBorder : AppTheme.lightBorder,
|
|
),
|
|
),
|
|
child: Text(
|
|
'${session.distance}m',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w700,
|
|
color: isDark ? AppTheme.darkTextPrimary : AppTheme.lightTextPrimary,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'${session.totalShots} tirs',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: isDark ? AppTheme.darkTextMuted : AppTheme.lightTextMuted,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
void _navigateToSettings(BuildContext context) async {
|
|
await Navigator.push(
|
|
context,
|
|
MaterialPageRoute(builder: (_) => const SettingsScreen()),
|
|
);
|
|
_loadStats();
|
|
}
|
|
} |