premier app version beta

This commit is contained in:
2026-01-18 13:38:09 +01:00
commit 031d4a4e17
164 changed files with 13698 additions and 0 deletions

View File

@@ -0,0 +1,244 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:intl/intl.dart';
import '../../../core/constants/app_constants.dart';
import '../../../core/theme/app_theme.dart';
import '../../../data/models/session.dart';
class HistoryChart extends StatelessWidget {
final List<Session> sessions;
const HistoryChart({
super.key,
required this.sessions,
});
@override
Widget build(BuildContext context) {
if (sessions.length < 2) {
return const SizedBox.shrink();
}
// Sort sessions by date and take last 10
final sortedSessions = List<Session>.from(sessions)
..sort((a, b) => a.createdAt.compareTo(b.createdAt));
final displaySessions = sortedSessions.length > 10
? sortedSessions.sublist(sortedSessions.length - 10)
: sortedSessions;
return Card(
child: Padding(
padding: const EdgeInsets.all(AppConstants.defaultPadding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.show_chart, color: AppTheme.primaryColor),
const SizedBox(width: 8),
Text(
'Evolution des scores',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
SizedBox(
height: 200,
child: LineChart(
LineChartData(
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: 20,
getDrawingHorizontalLine: (value) {
return FlLine(
color: Colors.grey[300],
strokeWidth: 1,
);
},
),
titlesData: FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
interval: 1,
getTitlesWidget: (value, meta) {
final index = value.toInt();
if (index < 0 || index >= displaySessions.length) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
DateFormat('dd/MM').format(displaySessions[index].createdAt),
style: const TextStyle(fontSize: 10),
),
);
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
interval: 20,
getTitlesWidget: (value, meta) {
return Text(
value.toInt().toString(),
style: const TextStyle(fontSize: 10),
);
},
),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
borderData: FlBorderData(
show: true,
border: Border(
bottom: BorderSide(color: Colors.grey[300]!),
left: BorderSide(color: Colors.grey[300]!),
),
),
minX: 0,
maxX: (displaySessions.length - 1).toDouble(),
minY: 0,
maxY: _getMaxY(displaySessions),
lineBarsData: [
// Score line
LineChartBarData(
spots: displaySessions.asMap().entries.map((entry) {
return FlSpot(
entry.key.toDouble(),
entry.value.totalScore.toDouble(),
);
}).toList(),
isCurved: true,
color: AppTheme.primaryColor,
barWidth: 3,
isStrokeCapRound: true,
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
return FlDotCirclePainter(
radius: 4,
color: AppTheme.primaryColor,
strokeWidth: 2,
strokeColor: Colors.white,
);
},
),
belowBarData: BarAreaData(
show: true,
color: AppTheme.primaryColor.withValues(alpha: 0.1),
),
),
],
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
getTooltipItems: (touchedSpots) {
return touchedSpots.map((spot) {
final session = displaySessions[spot.x.toInt()];
return LineTooltipItem(
'Score: ${session.totalScore}\n${session.shotCount} tirs',
const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
);
}).toList();
},
),
),
),
),
),
const SizedBox(height: 8),
_buildLegend(context, displaySessions),
],
),
),
);
}
double _getMaxY(List<Session> sessions) {
double maxScore = 0;
for (final session in sessions) {
if (session.totalScore > maxScore) {
maxScore = session.totalScore.toDouble();
}
}
return (maxScore * 1.2).ceilToDouble();
}
Widget _buildLegend(BuildContext context, List<Session> displaySessions) {
final avgScore = displaySessions.fold<int>(0, (sum, s) => sum + s.totalScore) /
displaySessions.length;
final trend = displaySessions.length >= 2
? displaySessions.last.totalScore - displaySessions.first.totalScore
: 0;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildLegendItem(
context,
'Moyenne',
avgScore.toStringAsFixed(1),
Icons.analytics,
AppTheme.primaryColor,
),
_buildLegendItem(
context,
'Tendance',
trend >= 0 ? '+$trend' : '$trend',
trend >= 0 ? Icons.trending_up : Icons.trending_down,
trend >= 0 ? AppTheme.successColor : AppTheme.errorColor,
),
_buildLegendItem(
context,
'Sessions',
'${displaySessions.length}',
Icons.list,
Colors.grey,
),
],
);
}
Widget _buildLegendItem(
BuildContext context,
String label,
String value,
IconData icon,
Color color,
) {
return Column(
children: [
Icon(icon, size: 16, color: color),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontWeight: FontWeight.bold,
color: color,
),
),
Text(
label,
style: Theme.of(context).textTheme.bodySmall,
),
],
);
}
}

View File

@@ -0,0 +1,148 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../../core/theme/app_theme.dart';
import '../../../data/models/session.dart';
import '../../../data/models/target_type.dart';
class SessionListItem extends StatelessWidget {
final Session session;
final VoidCallback? onTap;
final VoidCallback? onDelete;
const SessionListItem({
super.key,
required this.session,
this.onTap,
this.onDelete,
});
@override
Widget build(BuildContext context) {
return Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
// Thumbnail
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: SizedBox(
width: 60,
height: 60,
child: _buildThumbnail(),
),
),
const SizedBox(width: 12),
// Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
_getTargetIcon(),
size: 16,
color: AppTheme.primaryColor,
),
const SizedBox(width: 4),
Text(
session.targetType.displayName,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 4),
Text(
DateFormat('dd/MM/yyyy HH:mm').format(session.createdAt),
style: Theme.of(context).textTheme.bodySmall,
),
if (session.notes != null && session.notes!.isNotEmpty) ...[
const SizedBox(height: 2),
Text(
session.notes!,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
// Score
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${session.totalScore}',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
),
),
Text(
'${session.shotCount} tirs',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
// Delete button
if (onDelete != null)
IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: onDelete,
color: Colors.grey,
iconSize: 20,
),
],
),
),
),
);
}
Widget _buildThumbnail() {
final file = File(session.imagePath);
if (file.existsSync()) {
return Image.file(
file,
fit: BoxFit.cover,
errorBuilder: (_, _, _) => _buildPlaceholder(),
);
}
return _buildPlaceholder();
}
Widget _buildPlaceholder() {
return Container(
color: Colors.grey[200],
child: Icon(
_getTargetIcon(),
color: Colors.grey[400],
),
);
}
IconData _getTargetIcon() {
switch (session.targetType) {
case TargetType.concentric:
return Icons.track_changes;
case TargetType.silhouette:
return Icons.person;
}
}
}