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

@@ -80,12 +80,13 @@ class StatisticDao extends DatabaseAccessor<AppDatabase>
result.map((row) async { result.map((row) async {
final groups = await db.statisticGroupDao.getGroupsForStatistic(row.id); final groups = await db.statisticGroupDao.getGroupsForStatistic(row.id);
final games = await db.statisticGameDao.getGamesForStatistic(row.id); final games = await db.statisticGameDao.getGamesForStatistic(row.id);
final scopes = await db.statisticScopeDao.getScopeForStatistic(row.id);
return Statistic( return Statistic(
type: StatisticType.values.firstWhere( type: StatisticType.values.firstWhere(
(type) => type.name == row.type, (type) => type.name == row.type,
), ),
scopes: [], scopes: scopes,
timeframe: Timeframe.values.firstWhereOrNull( timeframe: Timeframe.values.firstWhereOrNull(
(t) => t.name == row.timeframe, (t) => t.name == row.timeframe,
), ),

View File

@@ -12,12 +12,13 @@ class StatisticGameDao extends DatabaseAccessor<AppDatabase>
StatisticGameDao(super.db); StatisticGameDao(super.db);
/// Retrieves a list of games associated with a specific statistic. /// Retrieves a list of games associated with a specific statistic.
Future<List<Game>> getGamesForStatistic(String statisticId) async { Future<List<Game>?> getGamesForStatistic(String statisticId) async {
final query = select(statisticGameTable).join([ final query = select(statisticGameTable).join([
innerJoin(gameTable, gameTable.id.equalsExp(statisticGameTable.gameId)), innerJoin(gameTable, gameTable.id.equalsExp(statisticGameTable.gameId)),
])..where(statisticGameTable.statisticId.equals(statisticId)); ])..where(statisticGameTable.statisticId.equals(statisticId));
final results = await query.map((row) => row.readTable(gameTable)).get(); final results = await query.map((row) => row.readTable(gameTable)).get();
if (results.isEmpty) return null;
return results return results
.map( .map(
(row) => Game( (row) => Game(

View File

@@ -12,7 +12,7 @@ class StatisticGroupDao extends DatabaseAccessor<AppDatabase>
StatisticGroupDao(super.db); StatisticGroupDao(super.db);
/// Retrieves a list of groups associated with a specific statistic. /// Retrieves a list of groups associated with a specific statistic.
Future<List<Group>> getGroupsForStatistic(String statisticId) async { Future<List<Group>?> getGroupsForStatistic(String statisticId) async {
final query = select(statisticGroupTable).join([ final query = select(statisticGroupTable).join([
innerJoin( innerJoin(
groupTable, groupTable,
@@ -21,6 +21,7 @@ class StatisticGroupDao extends DatabaseAccessor<AppDatabase>
])..where(statisticGroupTable.statisticId.equals(statisticId)); ])..where(statisticGroupTable.statisticId.equals(statisticId));
final results = await query.map((row) => row.readTable(groupTable)).get(); final results = await query.map((row) => row.readTable(groupTable)).get();
if (results.isEmpty) return null;
final groups = await Future.wait( final groups = await Future.wait(
results.map((result) async { results.map((result) async {
final groupMembers = await db.playerGroupDao.getPlayersOfGroup( final groupMembers = await db.playerGroupDao.getPlayersOfGroup(

View File

@@ -15,8 +15,8 @@ class StatisticScopeDao extends DatabaseAccessor<AppDatabase>
final query = select(statisticScopeTable) final query = select(statisticScopeTable)
..where((tbl) => tbl.statisticId.equals(statisticId)); ..where((tbl) => tbl.statisticId.equals(statisticId));
final results = await query.get(); final result = await query.get();
return results return result
.map( .map(
(row) => StatisticScope.values.firstWhere( (row) => StatisticScope.values.firstWhere(
(e) => e.name == row.scope, (e) => e.name == row.scope,

View File

@@ -21,6 +21,13 @@
"color_yellow": "Gelb", "color_yellow": "Gelb",
"confirm": "Bestätigen", "confirm": "Bestätigen",
"could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden", "could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden",
"@could_not_add_player": {
"placeholders": {
"playerName": {
"type": "String"
}
}
},
"create_game": "Spielvorlage erstellen", "create_game": "Spielvorlage erstellen",
"create_group": "Gruppe erstellen", "create_group": "Gruppe erstellen",
"create_match": "Spiel erstellen", "create_match": "Spiel erstellen",

View File

@@ -20,23 +20,29 @@
"color_teal": "Teal", "color_teal": "Teal",
"color_yellow": "Yellow", "color_yellow": "Yellow",
"confirm": "Confirm", "confirm": "Confirm",
"could_not_add_player": "Could not add player", "could_not_add_player": "Could not add player {playerName}",
"@could_not_add_player": {
"placeholders": {
"playerName": {
"type": "String"
}
}
},
"create_game": "Create Game", "create_game": "Create Game",
"create_group": "Create Group", "create_group": "Create Group",
"create_match": "Create match", "create_match": "Create match",
"create_new_group": "Create new group", "create_new_group": "Create new group",
"create_new_match": "Create new match", "create_new_match": "Create new match",
"create_statistic": "Create statistic", "create_statistic": "Create statistic",
"create_statistic_classifier_subtitle": "Select which key metric you want to display", "which_key_metric": "Select which key metric you want to display",
"create_statistic_classifier_title": "Classifier", "classifier": "Classifier",
"create_statistic_games_subtitle": "Select the filtered games", "select_the_filtered_games": "Select the filtered games",
"create_statistic_games_title": "Games", "games": "Games",
"create_statistic_groups_subtitle": "Select the filtered groups", "select_the_filtered_groups": "Select the filtered groups",
"create_statistic_groups_title": "Groups", "select_main_filter": "Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.",
"create_statistic_scope_subtitle": "Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.", "scope": "Scope",
"create_statistic_scope_title": "Scope", "select_a_timeframe_for_which_data_will_be_filtered": "Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.",
"create_statistic_timeframe_subtitle": "Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.", "timeframe": "Timeframe",
"create_statistic_timeframe_title": "Timeframe",
"created_on": "Created on", "created_on": "Created on",
"data": "Data", "data": "Data",
"data_successfully_deleted": "Data successfully deleted", "data_successfully_deleted": "Data successfully deleted",
@@ -54,6 +60,7 @@
} }
} }
}, },
"filter": "Filter",
"delete_group": "Delete Group", "delete_group": "Delete Group",
"delete_match": "Delete Match", "delete_match": "Delete Match",
"delete_player": "Delete player?", "delete_player": "Delete player?",
@@ -120,6 +127,7 @@
"no_results_entered_yet": "No results entered yet", "no_results_entered_yet": "No results entered yet",
"no_second_match_available": "No second match available", "no_second_match_available": "No second match available",
"no_statistics_available": "No statistics available", "no_statistics_available": "No statistics available",
"no_statistics_created_yet": "No statistics created yet",
"none": "None", "none": "None",
"none_group": "None", "none_group": "None",
"not_available": "Not available", "not_available": "Not available",

View File

@@ -221,8 +221,8 @@ abstract class AppLocalizations {
/// No description provided for @could_not_add_player. /// No description provided for @could_not_add_player.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Could not add player'** /// **'Could not add player {playerName}'**
String could_not_add_player(Object playerName); String could_not_add_player(String playerName);
/// No description provided for @create_game. /// No description provided for @create_game.
/// ///
@@ -260,65 +260,59 @@ abstract class AppLocalizations {
/// **'Create statistic'** /// **'Create statistic'**
String get create_statistic; String get create_statistic;
/// No description provided for @create_statistic_classifier_subtitle. /// No description provided for @which_key_metric.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Select which key metric you want to display'** /// **'Select which key metric you want to display'**
String get create_statistic_classifier_subtitle; String get which_key_metric;
/// No description provided for @create_statistic_classifier_title. /// No description provided for @classifier.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Classifier'** /// **'Classifier'**
String get create_statistic_classifier_title; String get classifier;
/// No description provided for @create_statistic_games_subtitle. /// No description provided for @select_the_filtered_games.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Select the filtered games'** /// **'Select the filtered games'**
String get create_statistic_games_subtitle; String get select_the_filtered_games;
/// No description provided for @create_statistic_games_title. /// No description provided for @games.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Games'** /// **'Games'**
String get create_statistic_games_title; String get games;
/// No description provided for @create_statistic_groups_subtitle. /// No description provided for @select_the_filtered_groups.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Select the filtered groups'** /// **'Select the filtered groups'**
String get create_statistic_groups_subtitle; String get select_the_filtered_groups;
/// No description provided for @create_statistic_groups_title. /// No description provided for @select_main_filter.
///
/// In en, this message translates to:
/// **'Groups'**
String get create_statistic_groups_title;
/// No description provided for @create_statistic_scope_subtitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.'** /// **'Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.'**
String get create_statistic_scope_subtitle; String get select_main_filter;
/// No description provided for @create_statistic_scope_title. /// No description provided for @scope.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Scope'** /// **'Scope'**
String get create_statistic_scope_title; String get scope;
/// No description provided for @create_statistic_timeframe_subtitle. /// No description provided for @select_a_timeframe_for_which_data_will_be_filtered.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.'** /// **'Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.'**
String get create_statistic_timeframe_subtitle; String get select_a_timeframe_for_which_data_will_be_filtered;
/// No description provided for @create_statistic_timeframe_title. /// No description provided for @timeframe.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Timeframe'** /// **'Timeframe'**
String get create_statistic_timeframe_title; String get timeframe;
/// No description provided for @created_on. /// No description provided for @created_on.
/// ///
@@ -380,6 +374,12 @@ abstract class AppLocalizations {
/// **'If you delete this game template, {count, plural, =1{1 match} other{{count} matches}} using this game template will also be deleted.'** /// **'If you delete this game template, {count, plural, =1{1 match} other{{count} matches}} using this game template will also be deleted.'**
String delete_game_with_matches_warning(int count); String delete_game_with_matches_warning(int count);
/// No description provided for @filter.
///
/// In en, this message translates to:
/// **'Filter'**
String get filter;
/// No description provided for @delete_group. /// No description provided for @delete_group.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -776,6 +776,12 @@ abstract class AppLocalizations {
/// **'No statistics available'** /// **'No statistics available'**
String get no_statistics_available; String get no_statistics_available;
/// No description provided for @no_statistics_created_yet.
///
/// In en, this message translates to:
/// **'No statistics created yet'**
String get no_statistics_created_yet;
/// No description provided for @none. /// No description provided for @none.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View File

@@ -69,7 +69,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get confirm => 'Bestätigen'; String get confirm => 'Bestätigen';
@override @override
String could_not_add_player(Object playerName) { String could_not_add_player(String playerName) {
return 'Spieler:in $playerName konnte nicht hinzugefügt werden'; return 'Spieler:in $playerName konnte nicht hinzugefügt werden';
} }
@@ -92,39 +92,33 @@ class AppLocalizationsDe extends AppLocalizations {
String get create_statistic => 'Statistik erstellen'; String get create_statistic => 'Statistik erstellen';
@override @override
String get create_statistic_classifier_subtitle => String get which_key_metric => 'Select which key metric you want to display';
'Wähle die anzuzeigende Hauptmetrik aus';
@override @override
String get create_statistic_classifier_title => 'Klassifikator'; String get classifier => 'Classifier';
@override @override
String get create_statistic_games_subtitle => String get select_the_filtered_games => 'Select the filtered games';
'Wähle die gefilterten Spielvorlagen';
@override @override
String get create_statistic_games_title => 'Spielvorlagen'; String get games => 'Games';
@override @override
String get create_statistic_groups_subtitle => String get select_the_filtered_groups => 'Select the filtered groups';
'Wähle die gefilterten Gruppen';
@override @override
String get create_statistic_groups_title => 'Gruppen'; String get select_main_filter =>
'Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.';
@override @override
String get create_statistic_scope_subtitle => String get scope => 'Scope';
'Wähle den Hauptfilter für deine Statistik. Er bestimmt, welche Daten zur Berechnung des Klassifikators verwendet werden.';
@override @override
String get create_statistic_scope_title => 'Bereich'; String get select_a_timeframe_for_which_data_will_be_filtered =>
'Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.';
@override @override
String get create_statistic_timeframe_subtitle => String get timeframe => 'Timeframe';
'Wähle einen Zeitraum, nach dem die Daten gefiltert werden. Nur Spiele, die innerhalb des Zeitraums beendet wurden, fließen in die Statistik ein.';
@override
String get create_statistic_timeframe_title => 'Zeitraum';
@override @override
String get created_on => 'Erstellt am'; String get created_on => 'Erstellt am';
@@ -166,6 +160,9 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Wenn du diese Spielvorlage löschst, $_temp0 mit dieser Spielvorlage ebenfalls gelöscht.'; return 'Wenn du diese Spielvorlage löschst, $_temp0 mit dieser Spielvorlage ebenfalls gelöscht.';
} }
@override
String get filter => 'Filter';
@override @override
String get delete_group => 'Gruppe löschen'; String get delete_group => 'Gruppe löschen';
@@ -369,6 +366,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get no_statistics_available => 'Keine Statistiken verfügbar'; String get no_statistics_available => 'Keine Statistiken verfügbar';
@override
String get no_statistics_created_yet => 'No statistics created yet';
@override @override
String get none => 'Kein'; String get none => 'Kein';

View File

@@ -69,8 +69,8 @@ class AppLocalizationsEn extends AppLocalizations {
String get confirm => 'Confirm'; String get confirm => 'Confirm';
@override @override
String could_not_add_player(Object playerName) { String could_not_add_player(String playerName) {
return 'Could not add player'; return 'Could not add player $playerName';
} }
@override @override
@@ -92,37 +92,33 @@ class AppLocalizationsEn extends AppLocalizations {
String get create_statistic => 'Create statistic'; String get create_statistic => 'Create statistic';
@override @override
String get create_statistic_classifier_subtitle => String get which_key_metric => 'Select which key metric you want to display';
'Select which key metric you want to display';
@override @override
String get create_statistic_classifier_title => 'Classifier'; String get classifier => 'Classifier';
@override @override
String get create_statistic_games_subtitle => 'Select the filtered games'; String get select_the_filtered_games => 'Select the filtered games';
@override @override
String get create_statistic_games_title => 'Games'; String get games => 'Games';
@override @override
String get create_statistic_groups_subtitle => 'Select the filtered groups'; String get select_the_filtered_groups => 'Select the filtered groups';
@override @override
String get create_statistic_groups_title => 'Groups'; String get select_main_filter =>
@override
String get create_statistic_scope_subtitle =>
'Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.'; 'Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.';
@override @override
String get create_statistic_scope_title => 'Scope'; String get scope => 'Scope';
@override @override
String get create_statistic_timeframe_subtitle => String get select_a_timeframe_for_which_data_will_be_filtered =>
'Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.'; 'Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.';
@override @override
String get create_statistic_timeframe_title => 'Timeframe'; String get timeframe => 'Timeframe';
@override @override
String get created_on => 'Created on'; String get created_on => 'Created on';
@@ -164,6 +160,9 @@ class AppLocalizationsEn extends AppLocalizations {
return 'If you delete this game template, $_temp0 using this game template will also be deleted.'; return 'If you delete this game template, $_temp0 using this game template will also be deleted.';
} }
@override
String get filter => 'Filter';
@override @override
String get delete_group => 'Delete Group'; String get delete_group => 'Delete Group';
@@ -367,6 +366,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get no_statistics_available => 'No statistics available'; String get no_statistics_available => 'No statistics available';
@override
String get no_statistics_created_yet => 'No statistics created yet';
@override @override
String get none => 'None'; String get none => 'None';

View File

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

View File

@@ -68,7 +68,7 @@ class _CreateStatisticViewState extends State<CreateStatisticView> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
loc.create_statistic_classifier_title, loc.classifier,
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: const TextStyle( style: const TextStyle(
color: CustomTheme.textColor, color: CustomTheme.textColor,
@@ -77,7 +77,7 @@ class _CreateStatisticViewState extends State<CreateStatisticView> {
), ),
), ),
Text( Text(
loc.create_statistic_classifier_subtitle, loc.select_a_classifier,
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: const TextStyle( style: const TextStyle(
color: CustomTheme.textColor, color: CustomTheme.textColor,
@@ -139,7 +139,7 @@ class _CreateStatisticViewState extends State<CreateStatisticView> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
loc.create_statistic_scope_title, loc.scope,
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: const TextStyle( style: const TextStyle(
color: CustomTheme.textColor, color: CustomTheme.textColor,
@@ -148,7 +148,7 @@ class _CreateStatisticViewState extends State<CreateStatisticView> {
), ),
), ),
Text( Text(
loc.create_statistic_scope_subtitle, loc.select_a_scope,
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: const TextStyle( style: const TextStyle(
color: CustomTheme.textColor, color: CustomTheme.textColor,
@@ -214,7 +214,7 @@ class _CreateStatisticViewState extends State<CreateStatisticView> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
loc.create_statistic_games_title, loc.games,
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: const TextStyle( style: const TextStyle(
color: CustomTheme.textColor, color: CustomTheme.textColor,
@@ -223,7 +223,7 @@ class _CreateStatisticViewState extends State<CreateStatisticView> {
), ),
), ),
Text( Text(
loc.create_statistic_games_subtitle, loc.select_the_filtered_games,
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: const TextStyle( style: const TextStyle(
color: CustomTheme.textColor, color: CustomTheme.textColor,
@@ -310,7 +310,7 @@ class _CreateStatisticViewState extends State<CreateStatisticView> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
loc.create_statistic_groups_title, loc.groups,
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: const TextStyle( style: const TextStyle(
color: CustomTheme.textColor, color: CustomTheme.textColor,
@@ -319,7 +319,7 @@ class _CreateStatisticViewState extends State<CreateStatisticView> {
), ),
), ),
Text( Text(
loc.create_statistic_groups_subtitle, loc.select_the_filtered_groups,
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: const TextStyle( style: const TextStyle(
color: CustomTheme.textColor, color: CustomTheme.textColor,
@@ -396,7 +396,7 @@ class _CreateStatisticViewState extends State<CreateStatisticView> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
loc.create_statistic_timeframe_title, loc.timeframe,
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: const TextStyle( style: const TextStyle(
color: CustomTheme.textColor, color: CustomTheme.textColor,
@@ -405,7 +405,7 @@ class _CreateStatisticViewState extends State<CreateStatisticView> {
), ),
), ),
Text( Text(
loc.create_statistic_timeframe_subtitle, loc.select_a_timeframe_for_which_data_will_be_filtered,
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: const TextStyle( style: const TextStyle(
color: CustomTheme.textColor, 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:flutter/material.dart';
import 'package:tallee/core/common.dart'; import 'package:tallee/core/common.dart';
import 'package:tallee/core/enums.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/match.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/statistic.dart'; import 'package:tallee/data/models/statistic.dart';
@@ -14,13 +16,18 @@ List<Color> _colorPalette = AppColor.values
.map((c) => getColorFromAppColor(c)) .map((c) => getColorFromAppColor(c))
.toList(); .toList();
/// Build the [StatisticsTile] for a given [Statistic]. /// Returns the icon for the given statistic type.
Widget buildStatisticTile({ 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 Statistic statistic,
required List<Match> matches, required List<Match> matches,
required List<Player> players, required List<Player> players,
required BuildContext context,
double? width,
}) { }) {
final filteredMatches = _getFilterMatches(statistic, matches); final filteredMatches = _getFilterMatches(statistic, matches);
final filteredPlayers = _getFilteredPlayers( final filteredPlayers = _getFilteredPlayers(
@@ -29,16 +36,26 @@ Widget buildStatisticTile({
filteredMatches, filteredMatches,
); );
print('Building tile for statistic: $statistic'); return _computeValuesForType(
print('Filtered matches count: ${filteredMatches.length}');
print('Filtered players count: ${filteredPlayers.length}');
final values = _computeValuesForType(
type: statistic.type, type: statistic.type,
matches: filteredMatches, matches: filteredMatches,
players: filteredPlayers, 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( return StatisticsTile(
icon: _getStatisticIcon(type: statistic.type), icon: _getStatisticIcon(type: statistic.type),
@@ -46,7 +63,9 @@ Widget buildStatisticTile({
width: width ?? MediaQuery.sizeOf(context).width * 0.95, width: width ?? MediaQuery.sizeOf(context).width * 0.95,
values: values, values: values,
barColor: _getStatisticColor(statistic), 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, width: MediaQuery.sizeOf(context).width * 0.95,
values: values, values: values,
barColor: _colorPalette[Random().nextInt(_colorPalette.length)], barColor: _colorPalette[Random().nextInt(_colorPalette.length)],
statistic: Statistic( selectedGames: [Game(name: 'Game 1', ruleset: Ruleset.highestScore)],
type: StatisticType.totalMatches, selectedGroups: [Group(name: 'Group 1', members: [])],
scopes: [StatisticScope.allPlayers], displayCount: 5,
timeframe: Timeframe.last7Days,
),
); );
} }

View File

@@ -8,9 +8,11 @@ import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/statistic.dart'; import 'package:tallee/data/models/statistic.dart';
import 'package:tallee/l10n/generated/app_localizations.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/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/views/main_menu/statistics_view/statistic_tile_factory.dart';
import 'package:tallee/presentation/widgets/app_skeleton.dart'; import 'package:tallee/presentation/widgets/app_skeleton.dart';
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
import 'package:tallee/presentation/widgets/top_centered_message.dart';
class StatisticsView extends StatefulWidget { class StatisticsView extends StatefulWidget {
/// A view that displays player statistics /// A view that displays player statistics
@@ -45,7 +47,16 @@ class _StatisticsViewState extends State<StatisticsView> {
alignment: AlignmentDirectional.bottomCenter, alignment: AlignmentDirectional.bottomCenter,
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
SingleChildScrollView( Visibility(
visible: statisticTiles.isNotEmpty,
replacement: Center(
child: TopCenteredMessage(
icon: Icons.info,
title: loc.info,
message: loc.no_statistics_created_yet,
),
),
child: SingleChildScrollView(
child: AppSkeleton( child: AppSkeleton(
enabled: isLoading, enabled: isLoading,
fixLayoutBuilder: true, fixLayoutBuilder: true,
@@ -55,11 +66,14 @@ class _StatisticsViewState extends State<StatisticsView> {
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
...statisticTiles, ...statisticTiles,
SizedBox(height: MediaQuery.paddingOf(context).bottom + 80), SizedBox(
height: MediaQuery.paddingOf(context).bottom + 80,
),
], ],
), ),
), ),
), ),
),
Positioned( Positioned(
bottom: MediaQuery.paddingOf(context).bottom + 20, bottom: MediaQuery.paddingOf(context).bottom + 20,
child: MainMenuButton( child: MainMenuButton(
@@ -75,12 +89,7 @@ class _StatisticsViewState extends State<StatisticsView> {
), ),
); );
if (!context.mounted) return; if (!context.mounted) return;
final newTile = buildStatisticTile( final newTile = _buildStatisticTile(context, newStatistic);
statistic: newStatistic,
matches: _allMatches,
players: _allPlayers,
context: context,
);
setState(() { setState(() {
statisticTiles.add(newTile); statisticTiles.add(newTile);
@@ -126,15 +135,40 @@ class _StatisticsViewState extends State<StatisticsView> {
setState(() { setState(() {
statisticTiles = [ statisticTiles = [
for (final statistic in statistics) ...[ for (final statistic in statistics) ...[
buildStatisticTile( _buildStatisticTile(context, statistic),
statistic: statistic,
matches: _allMatches,
players: _allPlayers,
context: context,
),
], ],
]; ];
isLoading = false; 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,
),
);
}
} }

View File

@@ -8,7 +8,6 @@ import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/player.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/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
@@ -27,7 +26,9 @@ class StatisticsTile extends StatelessWidget {
required this.width, required this.width,
required this.values, required this.values,
required this.barColor, required this.barColor,
required this.statistic, this.displayCount,
this.selectedGroups,
this.selectedGames,
}); });
/// The icon displayed next to the title. /// The icon displayed next to the title.
@@ -45,7 +46,10 @@ class StatisticsTile extends StatelessWidget {
/// The color of the bars representing the values. /// The color of the bars representing the values.
final Color barColor; final Color barColor;
final Statistic statistic; final int? displayCount;
final List<Group>? selectedGroups;
final List<Game>? selectedGames;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -70,8 +74,12 @@ class StatisticsTile extends StatelessWidget {
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final maxBarWidth = constraints.maxWidth * 0.8; final maxBarWidth = constraints.maxWidth * 0.8;
final displayCount = min(values.length, statistic.displayCount);
final displayValues = values.take(displayCount).toList(); // If displayCount wasnt provided, take all values
final valuesShown = displayCount == null
? values.length
: min(values.length, displayCount!);
final displayValues = values.take(valuesShown).toList();
final maxVal = displayValues.isNotEmpty final maxVal = displayValues.isNotEmpty
? displayValues.fold<num>( ? displayValues.fold<num>(
0, 0,
@@ -83,7 +91,7 @@ class StatisticsTile extends StatelessWidget {
return Column( return Column(
children: [ children: [
// Bars // Bars
...List.generate(displayCount, (index) { ...List.generate(valuesShown, (index) {
/// Fraction of wins /// Fraction of wins
final double fraction = (maxVal > 0) final double fraction = (maxVal > 0)
? (displayValues[index].$2 / maxVal) ? (displayValues[index].$2 / maxVal)
@@ -187,12 +195,14 @@ class StatisticsTile extends StatelessWidget {
}), }),
// Group & Game info // Group & Game info
if (statistic.selectedGames != null || if (hasGame || hasGroup)
statistic.selectedGroups != null)
Padding( Padding(
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
child: Row( child: Wrap(
mainAxisAlignment: MainAxisAlignment.start, alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 4,
runSpacing: 4,
children: [ children: [
// Game // Game
if (hasGroup) if (hasGroup)
@@ -205,7 +215,7 @@ class StatisticsTile extends StatelessWidget {
size: 20, size: 20,
), ),
Text( Text(
getGameText(statistic.selectedGames!), getGameText(selectedGames!),
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -227,7 +237,7 @@ class StatisticsTile extends StatelessWidget {
color: CustomTheme.hintColor, color: CustomTheme.hintColor,
), ),
Text( Text(
getGroupText(statistic.selectedGroups!), getGroupText(selectedGroups!),
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -265,9 +275,7 @@ class StatisticsTile extends StatelessWidget {
return text; return text;
} }
bool get hasGroup => bool get hasGroup => selectedGroups != null && selectedGroups!.isNotEmpty;
statistic.selectedGroups != null && statistic.selectedGroups!.isNotEmpty;
bool get hasGame => bool get hasGame => selectedGames != null && selectedGames!.isNotEmpty;
statistic.selectedGames != null && statistic.selectedGames!.isNotEmpty;
} }

View File

@@ -1,7 +1,7 @@
name: tallee name: tallee
description: "Tracking App for Card Games" description: "Tracking App for Card Games"
publish_to: 'none' publish_to: 'none'
version: 0.0.33+276 version: 0.0.33+280
environment: environment:
sdk: ^3.8.1 sdk: ^3.8.1