feat: statistic detail view
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 47s
Pull Request Pipeline / test (pull_request) Successful in 48s
Pull Request Pipeline / localizations (pull_request) Failing after 27s

This commit is contained in:
2026-05-25 00:39:01 +02:00
parent 72442b5375
commit bfb40d2eab
16 changed files with 406 additions and 143 deletions

View File

@@ -79,7 +79,7 @@ class _MatchViewState extends State<MatchView> {
visible: matches.isNotEmpty,
replacement: Center(
child: TopCenteredMessage(
icon: Icons.report,
icon: Icons.info,
title: loc.info,
message: loc.no_matches_created_yet,
),

View File

@@ -68,7 +68,7 @@ class _CreateStatisticViewState extends State<CreateStatisticView> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
loc.create_statistic_classifier_title,
loc.classifier,
textAlign: TextAlign.start,
style: const TextStyle(
color: CustomTheme.textColor,
@@ -77,7 +77,7 @@ class _CreateStatisticViewState extends State<CreateStatisticView> {
),
),
Text(
loc.create_statistic_classifier_subtitle,
loc.select_a_classifier,
textAlign: TextAlign.start,
style: const TextStyle(
color: CustomTheme.textColor,
@@ -139,7 +139,7 @@ class _CreateStatisticViewState extends State<CreateStatisticView> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
loc.create_statistic_scope_title,
loc.scope,
textAlign: TextAlign.start,
style: const TextStyle(
color: CustomTheme.textColor,
@@ -148,7 +148,7 @@ class _CreateStatisticViewState extends State<CreateStatisticView> {
),
),
Text(
loc.create_statistic_scope_subtitle,
loc.select_a_scope,
textAlign: TextAlign.start,
style: const TextStyle(
color: CustomTheme.textColor,
@@ -214,7 +214,7 @@ class _CreateStatisticViewState extends State<CreateStatisticView> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
loc.create_statistic_games_title,
loc.games,
textAlign: TextAlign.start,
style: const TextStyle(
color: CustomTheme.textColor,
@@ -223,7 +223,7 @@ class _CreateStatisticViewState extends State<CreateStatisticView> {
),
),
Text(
loc.create_statistic_games_subtitle,
loc.select_the_filtered_games,
textAlign: TextAlign.start,
style: const TextStyle(
color: CustomTheme.textColor,
@@ -310,7 +310,7 @@ class _CreateStatisticViewState extends State<CreateStatisticView> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
loc.create_statistic_groups_title,
loc.groups,
textAlign: TextAlign.start,
style: const TextStyle(
color: CustomTheme.textColor,
@@ -319,7 +319,7 @@ class _CreateStatisticViewState extends State<CreateStatisticView> {
),
),
Text(
loc.create_statistic_groups_subtitle,
loc.select_the_filtered_groups,
textAlign: TextAlign.start,
style: const TextStyle(
color: CustomTheme.textColor,
@@ -396,7 +396,7 @@ class _CreateStatisticViewState extends State<CreateStatisticView> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
loc.create_statistic_timeframe_title,
loc.timeframe,
textAlign: TextAlign.start,
style: const TextStyle(
color: CustomTheme.textColor,
@@ -405,7 +405,7 @@ class _CreateStatisticViewState extends State<CreateStatisticView> {
),
),
Text(
loc.create_statistic_timeframe_subtitle,
loc.select_a_timeframe_for_which_data_will_be_filtered,
textAlign: TextAlign.start,
style: const TextStyle(
color: CustomTheme.textColor,

View File

@@ -0,0 +1,178 @@
import 'package:flutter/material.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/statistic.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/statistics_view/create_statistic_view.dart'
show
translateScopeToString,
translateStatisticTypeToString,
translateTimeframeToString;
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
import 'package:tallee/presentation/widgets/tiles/statistics_tile.dart';
class StatisticDetailView extends StatefulWidget {
const StatisticDetailView({
super.key,
required this.statistic,
required this.values,
required this.icon,
required this.barColor,
});
final Statistic statistic;
final List<(Player, num)> values;
final IconData icon;
final Color barColor;
@override
State<StatisticDetailView> createState() => _StatisticDetailViewState();
}
class _StatisticDetailViewState extends State<StatisticDetailView> {
int displayCount = 0;
@override
void initState() {
super.initState();
displayCount = widget.statistic.displayCount;
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
final title = translateStatisticTypeToString(
widget.statistic.type,
context,
);
const style = TextStyle(fontWeight: FontWeight.bold);
return Scaffold(
appBar: AppBar(title: Text(title)),
body: SingleChildScrollView(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
StatisticsTile(
icon: widget.icon,
title: title,
width: MediaQuery.sizeOf(context).width * 0.95,
values: widget.values,
barColor: widget.barColor,
selectedGroups: widget.statistic.selectedGroups,
selectedGames: widget.statistic.selectedGames,
),
const SizedBox(height: 12),
InfoTile(
icon: Icons.filter_alt,
title: loc.filter,
content: Column(
spacing: 12,
children: [
// Scopes
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(loc.scope, style: style),
Text(
widget.statistic.scopes
.map(
(scope) => translateScopeToString(scope, context),
)
.join('\n'),
textAlign: TextAlign.end,
),
],
),
// Timeframe
if (widget.statistic.timeframe != null)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(loc.timeframe, style: style),
Text(
translateTimeframeToString(
widget.statistic.timeframe!,
context,
),
textAlign: TextAlign.end,
),
],
),
// Groups
if (widget.statistic.selectedGroups != null)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(loc.groups, style: style),
Text(
widget.statistic.selectedGroups!
.map((group) => group.name)
.join('\n'),
textAlign: TextAlign.end,
),
],
),
// Games
if (widget.statistic.selectedGames != null)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(loc.games, style: style),
Text(
widget.statistic.selectedGames!
.map((game) => game.name)
.join('\n'),
textAlign: TextAlign.end,
),
],
),
if (widget.values.isNotEmpty)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Display count', style: style),
Row(
children: [
HapticIconButton(
icon: const Icon(Icons.remove),
onPressed: displayCount <= 1
? null
: () => setState(() => displayCount -= 1),
),
SizedBox(
width: 30,
child: Text(
'$displayCount',
textAlign: TextAlign.center,
),
),
HapticIconButton(
icon: const Icon(Icons.add),
onPressed: displayCount >= widget.values.length
? null
: () => setState(() => displayCount += 1),
),
],
),
],
),
],
),
),
],
),
),
);
}
}

View File

@@ -3,6 +3,8 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/statistic.dart';
@@ -14,13 +16,18 @@ List<Color> _colorPalette = AppColor.values
.map((c) => getColorFromAppColor(c))
.toList();
/// Build the [StatisticsTile] for a given [Statistic].
Widget buildStatisticTile({
/// Returns the icon for the given statistic type.
IconData getStatisticIconForType(StatisticType type) =>
_getStatisticIcon(type: type);
/// Returns a color from the palette based on the statistic's ID.
Color getStatisticColorForStatistic(Statistic stat) => _getStatisticColor(stat);
/// Computes the statistic values for a given [Statistic].
List<(Player, num)> computeStatisticValues({
required Statistic statistic,
required List<Match> matches,
required List<Player> players,
required BuildContext context,
double? width,
}) {
final filteredMatches = _getFilterMatches(statistic, matches);
final filteredPlayers = _getFilteredPlayers(
@@ -29,16 +36,26 @@ Widget buildStatisticTile({
filteredMatches,
);
print('Building tile for statistic: $statistic');
print('Filtered matches count: ${filteredMatches.length}');
print('Filtered players count: ${filteredPlayers.length}');
final values = _computeValuesForType(
return _computeValuesForType(
type: statistic.type,
matches: filteredMatches,
players: filteredPlayers,
);
print(values);
}
/// Build the [StatisticsTile] for a given [Statistic].
Widget buildStatisticTile({
required Statistic statistic,
required List<Match> matches,
required List<Player> players,
required BuildContext context,
double? width,
}) {
final values = computeStatisticValues(
statistic: statistic,
matches: matches,
players: players,
);
return StatisticsTile(
icon: _getStatisticIcon(type: statistic.type),
@@ -46,7 +63,9 @@ Widget buildStatisticTile({
width: width ?? MediaQuery.sizeOf(context).width * 0.95,
values: values,
barColor: _getStatisticColor(statistic),
statistic: statistic,
displayCount: statistic.displayCount,
selectedGroups: statistic.selectedGroups,
selectedGames: statistic.selectedGames,
);
}
@@ -296,10 +315,8 @@ Widget buildSkeletonStatisticTile({required BuildContext context}) {
width: MediaQuery.sizeOf(context).width * 0.95,
values: values,
barColor: _colorPalette[Random().nextInt(_colorPalette.length)],
statistic: Statistic(
type: StatisticType.totalMatches,
scopes: [StatisticScope.allPlayers],
timeframe: Timeframe.last7Days,
),
selectedGames: [Game(name: 'Game 1', ruleset: Ruleset.highestScore)],
selectedGroups: [Group(name: 'Group 1', members: [])],
displayCount: 5,
);
}

View File

@@ -8,9 +8,11 @@ import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/statistic.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/statistics_view/create_statistic_view.dart';
import 'package:tallee/presentation/views/main_menu/statistics_view/statistic_detail_view.dart';
import 'package:tallee/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart';
import 'package:tallee/presentation/widgets/app_skeleton.dart';
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
import 'package:tallee/presentation/widgets/top_centered_message.dart';
class StatisticsView extends StatefulWidget {
/// A view that displays player statistics
@@ -45,18 +47,30 @@ class _StatisticsViewState extends State<StatisticsView> {
alignment: AlignmentDirectional.bottomCenter,
fit: StackFit.expand,
children: [
SingleChildScrollView(
child: AppSkeleton(
enabled: isLoading,
fixLayoutBuilder: true,
child: Column(
spacing: 12,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
...statisticTiles,
SizedBox(height: MediaQuery.paddingOf(context).bottom + 80),
],
Visibility(
visible: statisticTiles.isNotEmpty,
replacement: Center(
child: TopCenteredMessage(
icon: Icons.info,
title: loc.info,
message: loc.no_statistics_created_yet,
),
),
child: SingleChildScrollView(
child: AppSkeleton(
enabled: isLoading,
fixLayoutBuilder: true,
child: Column(
spacing: 12,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
...statisticTiles,
SizedBox(
height: MediaQuery.paddingOf(context).bottom + 80,
),
],
),
),
),
),
@@ -75,12 +89,7 @@ class _StatisticsViewState extends State<StatisticsView> {
),
);
if (!context.mounted) return;
final newTile = buildStatisticTile(
statistic: newStatistic,
matches: _allMatches,
players: _allPlayers,
context: context,
);
final newTile = _buildStatisticTile(context, newStatistic);
setState(() {
statisticTiles.add(newTile);
@@ -126,15 +135,40 @@ class _StatisticsViewState extends State<StatisticsView> {
setState(() {
statisticTiles = [
for (final statistic in statistics) ...[
buildStatisticTile(
statistic: statistic,
matches: _allMatches,
players: _allPlayers,
context: context,
),
_buildStatisticTile(context, statistic),
],
];
isLoading = false;
});
}
Widget _buildStatisticTile(BuildContext context, Statistic statistic) {
return GestureDetector(
onTap: () {
final values = computeStatisticValues(
statistic: statistic,
matches: _allMatches,
players: _allPlayers,
);
Navigator.push(
context,
adaptivePageRoute(
builder: (context) => StatisticDetailView(
statistic: statistic,
values: values,
icon: getStatisticIconForType(statistic.type),
barColor: getStatisticColorForStatistic(statistic),
),
),
);
},
child: buildStatisticTile(
statistic: statistic,
matches: _allMatches,
players: _allPlayers,
context: context,
),
);
}
}