From 134f77c5a3160eb5197c9906d9de8e638c9570d5 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 24 May 2026 01:26:08 +0200 Subject: [PATCH] feat: create statistics view --- lib/core/enums.dart | 28 + lib/data/models/statistic.dart | 19 + lib/l10n/arb/app_de.arb | 35 + lib/l10n/arb/app_en.arb | 35 + lib/l10n/generated/app_localizations.dart | 210 ++++++ lib/l10n/generated/app_localizations_de.dart | 110 +++ lib/l10n/generated/app_localizations_en.dart | 108 +++ .../main_menu/custom_navigation_bar.dart | 2 +- .../create_statistic_view.dart | 635 ++++++++++++++++++ .../statistics_view.dart | 169 +++-- pubspec.lock | 16 + pubspec.yaml | 2 + 12 files changed, 1296 insertions(+), 73 deletions(-) create mode 100644 lib/data/models/statistic.dart create mode 100644 lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart rename lib/presentation/views/main_menu/{ => statistics_view}/statistics_view.dart (62%) diff --git a/lib/core/enums.dart b/lib/core/enums.dart index 99141e4..5d46a3f 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -44,3 +44,31 @@ enum Ruleset { /// Different colors for highlighting games enum GameColor { red, orange, yellow, green, teal, blue, purple, pink } + +enum StatisticType { + totalMatches, + totalWins, + totalScore, + totalLosses, + averageScore, + bestScore, + worstScore, + winrate, +} + +enum StatisticScope { + allPlayers, + //selectedPlayer, + selectedGroups, + selectedGames, + timeframe, +} + +enum Timeframe { + last7Days, + last30Days, + last90Days, + last180Days, + lastYear, + allTime, +} diff --git a/lib/data/models/statistic.dart b/lib/data/models/statistic.dart new file mode 100644 index 0000000..4b5df07 --- /dev/null +++ b/lib/data/models/statistic.dart @@ -0,0 +1,19 @@ +import 'package:tallee/core/enums.dart'; +import 'package:tallee/data/models/game.dart'; +import 'package:tallee/data/models/group.dart'; + +class Statistic { + final StatisticType type; + final List scopes; + final Timeframe? timeframe; + final List? selectedGroups; + final List? selectedGames; + + Statistic({ + required this.type, + required this.scopes, + this.timeframe, + this.selectedGroups, + this.selectedGames, + }); +} diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 610d2c9..8f92cb0 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -26,6 +26,17 @@ "create_match": "Spiel erstellen", "create_new_group": "Neue Gruppe erstellen", "create_new_match": "Neues Spiel erstellen", + "create_statistic": "Statistik erstellen", + "create_statistic_classifier_subtitle": "Wähle die anzuzeigende Hauptmetrik aus", + "create_statistic_classifier_title": "Klassifikator", + "create_statistic_games_subtitle": "Wähle die gefilterten Spielvorlagen", + "create_statistic_games_title": "Spielvorlagen", + "create_statistic_groups_subtitle": "Wähle die gefilterten Gruppen", + "create_statistic_groups_title": "Gruppen", + "create_statistic_scope_subtitle": "Wähle den Hauptfilter für deine Statistik. Er bestimmt, welche Daten zur Berechnung des Klassifikators verwendet werden.", + "create_statistic_scope_title": "Bereich", + "create_statistic_timeframe_subtitle": "Wähle einen Zeitraum, nach dem die Daten gefiltert werden. Nur Spiele, die innerhalb des Zeitraums beendet wurden, fließen in die Statistik ein.", + "create_statistic_timeframe_title": "Zeitraum", "created_on": "Erstellt am", "data": "Daten", "data_successfully_deleted": "Daten erfolgreich gelöscht", @@ -82,6 +93,7 @@ "legal_notice": "Impressum", "licenses": "Lizenzen", "live_edit_mode": "Live-Bearbeitungsmodus", + "loading": "Lädt...", "loser": "Verlierer:in", "lowest_score": "Niedrigste Punkte", "match_in_progress": "Spiel läuft...", @@ -134,6 +146,11 @@ "save_changes": "Änderungen speichern", "search_for_groups": "Nach Gruppen suchen", "search_for_players": "Nach Spieler:innen suchen", + "select_a_classifier": "Klassifikator auswählen", + "select_a_game": "Spielvorlage auswählen", + "select_a_group": "Gruppe auswählen", + "select_a_scope": "Bereich auswählen", + "select_a_timeframe": "Zeitraum auswählen", "select_loser": "Verlierer:in wählen", "select_winner": "Gewinner:in wählen", "select_winners": "Gewinner:innen wählen", @@ -142,6 +159,18 @@ "settings": "Einstellungen", "single_loser": "Ein:e Verlierer:in", "single_winner": "Ein:e Gewinner:in", + "statistic_scope_all_players": "Alle Spieler:innen", + "statistic_scope_selected_games": "Ausgewählte Spielvorlagen", + "statistic_scope_selected_groups": "Ausgewählte Gruppen", + "statistic_scope_timeframe": "Zeitraum", + "statistic_type_average_score": "Durchschnittliche Punktzahl", + "statistic_type_best_score": "Beste Punktzahl", + "statistic_type_total_losses": "Niederlagen insgesamt", + "statistic_type_total_matches": "Spiele insgesamt", + "statistic_type_total_score": "Punktzahl insgesamt", + "statistic_type_total_wins": "Siege insgesamt", + "statistic_type_winrate": "Siegquote", + "statistic_type_worst_score": "Schlechteste Punktzahl", "statistics": "Statistiken", "stats": "Statistiken", "successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt", @@ -149,6 +178,12 @@ "there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht", "this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.", "tie": "Unentschieden", + "timeframe_all_time": "Gesamter Zeitraum", + "timeframe_last_180_days": "Letzte 180 Tage", + "timeframe_last_30_days": "Letzte 30 Tage", + "timeframe_last_7_days": "Letzte 7 Tage", + "timeframe_last_90_days": "Letzte 90 Tage", + "timeframe_last_year": "Letztes Jahr", "today_at": "Heute um", "undo": "Rückgängig", "unknown_exception": "Unbekannter Fehler (siehe Konsole)", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index a8b0634..b7548bd 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -26,6 +26,17 @@ "create_match": "Create match", "create_new_group": "Create new group", "create_new_match": "Create new match", + "create_statistic": "Create statistic", + "create_statistic_classifier_subtitle": "Select which key metric you want to display", + "create_statistic_classifier_title": "Classifier", + "create_statistic_games_subtitle": "Select the filtered games", + "create_statistic_games_title": "Games", + "create_statistic_groups_subtitle": "Select the filtered groups", + "create_statistic_groups_title": "Groups", + "create_statistic_scope_subtitle": "Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.", + "create_statistic_scope_title": "Scope", + "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.", + "create_statistic_timeframe_title": "Timeframe", "created_on": "Created on", "data": "Data", "data_successfully_deleted": "Data successfully deleted", @@ -82,6 +93,7 @@ "legal_notice": "Legal Notice", "licenses": "Licenses", "live_edit_mode": "Live Edit Mode", + "loading": "Loading...", "loser": "Loser", "lowest_score": "Lowest Score", "match_in_progress": "Match in progress...", @@ -139,10 +151,27 @@ "selected_players": "Selected players", "set_name": "Set name", "settings": "Settings", + "select_a_classifier": "Select a classifier", + "select_a_game": "Select a game", + "select_a_group": "Select a group", + "select_a_scope": "Select a scope", + "select_a_timeframe": "Select a timeframe", "single_loser": "Single Loser", "single_winner": "Single Winner", "statistics": "Statistics", "stats": "Stats", + "statistic_scope_all_players": "All players", + "statistic_scope_selected_games": "Selected games", + "statistic_scope_selected_groups": "Selected groups", + "statistic_scope_timeframe": "Timeframe", + "statistic_type_average_score": "Average score", + "statistic_type_best_score": "Best score", + "statistic_type_total_losses": "Total losses", + "statistic_type_total_matches": "Total matches", + "statistic_type_total_score": "Total score", + "statistic_type_total_wins": "Total wins", + "statistic_type_winrate": "Winrate", + "statistic_type_worst_score": "Worst score", "successfully_added_player": "Successfully added player {playerName}", "@successfully_added_player": { "description": "Success message when adding a player", @@ -157,6 +186,12 @@ "there_is_no_group_matching_your_search": "There is no group matching your search", "this_cannot_be_undone": "This can't be undone.", "tie": "Tie", + "timeframe_all_time": "All time", + "timeframe_last_180_days": "Last 180 days", + "timeframe_last_30_days": "Last 30 days", + "timeframe_last_7_days": "Last 7 days", + "timeframe_last_90_days": "Last 90 days", + "timeframe_last_year": "Last year", "today_at": "Today at", "undo": "Undo", "unknown_exception": "Unknown Exception (see console)", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 4cee263..e8af2e8 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -254,6 +254,72 @@ abstract class AppLocalizations { /// **'Create new match'** String get create_new_match; + /// No description provided for @create_statistic. + /// + /// In en, this message translates to: + /// **'Create statistic'** + String get create_statistic; + + /// No description provided for @create_statistic_classifier_subtitle. + /// + /// In en, this message translates to: + /// **'Select which key metric you want to display'** + String get create_statistic_classifier_subtitle; + + /// No description provided for @create_statistic_classifier_title. + /// + /// In en, this message translates to: + /// **'Classifier'** + String get create_statistic_classifier_title; + + /// No description provided for @create_statistic_games_subtitle. + /// + /// In en, this message translates to: + /// **'Select the filtered games'** + String get create_statistic_games_subtitle; + + /// No description provided for @create_statistic_games_title. + /// + /// In en, this message translates to: + /// **'Games'** + String get create_statistic_games_title; + + /// No description provided for @create_statistic_groups_subtitle. + /// + /// In en, this message translates to: + /// **'Select the filtered groups'** + String get create_statistic_groups_subtitle; + + /// No description provided for @create_statistic_groups_title. + /// + /// 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: + /// **'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; + + /// No description provided for @create_statistic_scope_title. + /// + /// In en, this message translates to: + /// **'Scope'** + String get create_statistic_scope_title; + + /// No description provided for @create_statistic_timeframe_subtitle. + /// + /// 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.'** + String get create_statistic_timeframe_subtitle; + + /// No description provided for @create_statistic_timeframe_title. + /// + /// In en, this message translates to: + /// **'Timeframe'** + String get create_statistic_timeframe_title; + /// No description provided for @created_on. /// /// In en, this message translates to: @@ -548,6 +614,12 @@ abstract class AppLocalizations { /// **'Live Edit Mode'** String get live_edit_mode; + /// No description provided for @loading. + /// + /// In en, this message translates to: + /// **'Loading...'** + String get loading; + /// No description provided for @loser. /// /// In en, this message translates to: @@ -890,6 +962,36 @@ abstract class AppLocalizations { /// **'Settings'** String get settings; + /// No description provided for @select_a_classifier. + /// + /// In en, this message translates to: + /// **'Select a classifier'** + String get select_a_classifier; + + /// No description provided for @select_a_game. + /// + /// In en, this message translates to: + /// **'Select a game'** + String get select_a_game; + + /// No description provided for @select_a_group. + /// + /// In en, this message translates to: + /// **'Select a group'** + String get select_a_group; + + /// No description provided for @select_a_scope. + /// + /// In en, this message translates to: + /// **'Select a scope'** + String get select_a_scope; + + /// No description provided for @select_a_timeframe. + /// + /// In en, this message translates to: + /// **'Select a timeframe'** + String get select_a_timeframe; + /// No description provided for @single_loser. /// /// In en, this message translates to: @@ -914,6 +1016,78 @@ abstract class AppLocalizations { /// **'Stats'** String get stats; + /// No description provided for @statistic_scope_all_players. + /// + /// In en, this message translates to: + /// **'All players'** + String get statistic_scope_all_players; + + /// No description provided for @statistic_scope_selected_games. + /// + /// In en, this message translates to: + /// **'Selected games'** + String get statistic_scope_selected_games; + + /// No description provided for @statistic_scope_selected_groups. + /// + /// In en, this message translates to: + /// **'Selected groups'** + String get statistic_scope_selected_groups; + + /// No description provided for @statistic_scope_timeframe. + /// + /// In en, this message translates to: + /// **'Timeframe'** + String get statistic_scope_timeframe; + + /// No description provided for @statistic_type_average_score. + /// + /// In en, this message translates to: + /// **'Average score'** + String get statistic_type_average_score; + + /// No description provided for @statistic_type_best_score. + /// + /// In en, this message translates to: + /// **'Best score'** + String get statistic_type_best_score; + + /// No description provided for @statistic_type_total_losses. + /// + /// In en, this message translates to: + /// **'Total losses'** + String get statistic_type_total_losses; + + /// No description provided for @statistic_type_total_matches. + /// + /// In en, this message translates to: + /// **'Total matches'** + String get statistic_type_total_matches; + + /// No description provided for @statistic_type_total_score. + /// + /// In en, this message translates to: + /// **'Total score'** + String get statistic_type_total_score; + + /// No description provided for @statistic_type_total_wins. + /// + /// In en, this message translates to: + /// **'Total wins'** + String get statistic_type_total_wins; + + /// No description provided for @statistic_type_winrate. + /// + /// In en, this message translates to: + /// **'Winrate'** + String get statistic_type_winrate; + + /// No description provided for @statistic_type_worst_score. + /// + /// In en, this message translates to: + /// **'Worst score'** + String get statistic_type_worst_score; + /// Success message when adding a player /// /// In en, this message translates to: @@ -944,6 +1118,42 @@ abstract class AppLocalizations { /// **'Tie'** String get tie; + /// No description provided for @timeframe_all_time. + /// + /// In en, this message translates to: + /// **'All time'** + String get timeframe_all_time; + + /// No description provided for @timeframe_last_180_days. + /// + /// In en, this message translates to: + /// **'Last 180 days'** + String get timeframe_last_180_days; + + /// No description provided for @timeframe_last_30_days. + /// + /// In en, this message translates to: + /// **'Last 30 days'** + String get timeframe_last_30_days; + + /// No description provided for @timeframe_last_7_days. + /// + /// In en, this message translates to: + /// **'Last 7 days'** + String get timeframe_last_7_days; + + /// No description provided for @timeframe_last_90_days. + /// + /// In en, this message translates to: + /// **'Last 90 days'** + String get timeframe_last_90_days; + + /// No description provided for @timeframe_last_year. + /// + /// In en, this message translates to: + /// **'Last year'** + String get timeframe_last_year; + /// No description provided for @today_at. /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 66dca88..4ff81bb 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -88,6 +88,44 @@ class AppLocalizationsDe extends AppLocalizations { @override String get create_new_match => 'Neues Spiel erstellen'; + @override + String get create_statistic => 'Statistik erstellen'; + + @override + String get create_statistic_classifier_subtitle => + 'Wähle die anzuzeigende Hauptmetrik aus'; + + @override + String get create_statistic_classifier_title => 'Klassifikator'; + + @override + String get create_statistic_games_subtitle => + 'Wähle die gefilterten Spielvorlagen'; + + @override + String get create_statistic_games_title => 'Spielvorlagen'; + + @override + String get create_statistic_groups_subtitle => + 'Wähle die gefilterten Gruppen'; + + @override + String get create_statistic_groups_title => 'Gruppen'; + + @override + String get create_statistic_scope_subtitle => + 'Wähle den Hauptfilter für deine Statistik. Er bestimmt, welche Daten zur Berechnung des Klassifikators verwendet werden.'; + + @override + String get create_statistic_scope_title => 'Bereich'; + + @override + String get create_statistic_timeframe_subtitle => + '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 String get created_on => 'Erstellt am'; @@ -249,6 +287,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get live_edit_mode => 'Live-Bearbeitungsmodus'; + @override + String get loading => 'Lädt...'; + @override String get loser => 'Verlierer:in'; @@ -426,6 +467,21 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings => 'Einstellungen'; + @override + String get select_a_classifier => 'Klassifikator auswählen'; + + @override + String get select_a_game => 'Spielvorlage auswählen'; + + @override + String get select_a_group => 'Gruppe auswählen'; + + @override + String get select_a_scope => 'Bereich auswählen'; + + @override + String get select_a_timeframe => 'Zeitraum auswählen'; + @override String get single_loser => 'Ein:e Verlierer:in'; @@ -438,6 +494,42 @@ class AppLocalizationsDe extends AppLocalizations { @override String get stats => 'Statistiken'; + @override + String get statistic_scope_all_players => 'Alle Spieler:innen'; + + @override + String get statistic_scope_selected_games => 'Ausgewählte Spielvorlagen'; + + @override + String get statistic_scope_selected_groups => 'Ausgewählte Gruppen'; + + @override + String get statistic_scope_timeframe => 'Zeitraum'; + + @override + String get statistic_type_average_score => 'Durchschnittliche Punktzahl'; + + @override + String get statistic_type_best_score => 'Beste Punktzahl'; + + @override + String get statistic_type_total_losses => 'Niederlagen insgesamt'; + + @override + String get statistic_type_total_matches => 'Spiele insgesamt'; + + @override + String get statistic_type_total_score => 'Punktzahl insgesamt'; + + @override + String get statistic_type_total_wins => 'Siege insgesamt'; + + @override + String get statistic_type_winrate => 'Siegquote'; + + @override + String get statistic_type_worst_score => 'Schlechteste Punktzahl'; + @override String successfully_added_player(String playerName) { return 'Spieler:in $playerName erfolgreich hinzugefügt'; @@ -458,6 +550,24 @@ class AppLocalizationsDe extends AppLocalizations { @override String get tie => 'Unentschieden'; + @override + String get timeframe_all_time => 'Gesamter Zeitraum'; + + @override + String get timeframe_last_180_days => 'Letzte 180 Tage'; + + @override + String get timeframe_last_30_days => 'Letzte 30 Tage'; + + @override + String get timeframe_last_7_days => 'Letzte 7 Tage'; + + @override + String get timeframe_last_90_days => 'Letzte 90 Tage'; + + @override + String get timeframe_last_year => 'Letztes Jahr'; + @override String get today_at => 'Heute um'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index b9e467a..4bc1dd7 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -88,6 +88,42 @@ class AppLocalizationsEn extends AppLocalizations { @override String get create_new_match => 'Create new match'; + @override + String get create_statistic => 'Create statistic'; + + @override + String get create_statistic_classifier_subtitle => + 'Select which key metric you want to display'; + + @override + String get create_statistic_classifier_title => 'Classifier'; + + @override + String get create_statistic_games_subtitle => 'Select the filtered games'; + + @override + String get create_statistic_games_title => 'Games'; + + @override + String get create_statistic_groups_subtitle => 'Select the filtered groups'; + + @override + String get create_statistic_groups_title => 'Groups'; + + @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.'; + + @override + String get create_statistic_scope_title => 'Scope'; + + @override + String get 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.'; + + @override + String get create_statistic_timeframe_title => 'Timeframe'; + @override String get created_on => 'Created on'; @@ -249,6 +285,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get live_edit_mode => 'Live Edit Mode'; + @override + String get loading => 'Loading...'; + @override String get loser => 'Loser'; @@ -426,6 +465,21 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settings => 'Settings'; + @override + String get select_a_classifier => 'Select a classifier'; + + @override + String get select_a_game => 'Select a game'; + + @override + String get select_a_group => 'Select a group'; + + @override + String get select_a_scope => 'Select a scope'; + + @override + String get select_a_timeframe => 'Select a timeframe'; + @override String get single_loser => 'Single Loser'; @@ -438,6 +492,42 @@ class AppLocalizationsEn extends AppLocalizations { @override String get stats => 'Stats'; + @override + String get statistic_scope_all_players => 'All players'; + + @override + String get statistic_scope_selected_games => 'Selected games'; + + @override + String get statistic_scope_selected_groups => 'Selected groups'; + + @override + String get statistic_scope_timeframe => 'Timeframe'; + + @override + String get statistic_type_average_score => 'Average score'; + + @override + String get statistic_type_best_score => 'Best score'; + + @override + String get statistic_type_total_losses => 'Total losses'; + + @override + String get statistic_type_total_matches => 'Total matches'; + + @override + String get statistic_type_total_score => 'Total score'; + + @override + String get statistic_type_total_wins => 'Total wins'; + + @override + String get statistic_type_winrate => 'Winrate'; + + @override + String get statistic_type_worst_score => 'Worst score'; + @override String successfully_added_player(String playerName) { return 'Successfully added player $playerName'; @@ -457,6 +547,24 @@ class AppLocalizationsEn extends AppLocalizations { @override String get tie => 'Tie'; + @override + String get timeframe_all_time => 'All time'; + + @override + String get timeframe_last_180_days => 'Last 180 days'; + + @override + String get timeframe_last_30_days => 'Last 30 days'; + + @override + String get timeframe_last_7_days => 'Last 7 days'; + + @override + String get timeframe_last_90_days => 'Last 90 days'; + + @override + String get timeframe_last_year => 'Last year'; + @override String get today_at => 'Today at'; diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index 7e5434b..07d66b4 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -6,7 +6,7 @@ import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/group_view/group_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_view.dart'; import 'package:tallee/presentation/views/main_menu/settings_view/settings_view.dart'; -import 'package:tallee/presentation/views/main_menu/statistics_view.dart'; +import 'package:tallee/presentation/views/main_menu/statistics_view/statistics_view.dart'; import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart'; import 'package:tallee/presentation/widgets/navbar_item.dart'; diff --git a/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart b/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart new file mode 100644 index 0000000..17706a1 --- /dev/null +++ b/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart @@ -0,0 +1,635 @@ +import 'package:animated_custom_dropdown/custom_dropdown.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:tallee/core/common.dart'; +import 'package:tallee/core/constants.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/models/game.dart'; +import 'package:tallee/data/models/group.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/widgets/buttons/animated_dialog_button.dart'; + +class CreateStatisticView extends StatefulWidget { + const CreateStatisticView({super.key, required this.onStatisticCreated}); + + final void Function() onStatisticCreated; + + @override + State createState() => _CreateStatisticViewState(); +} + +class _CreateStatisticViewState extends State { + bool isLoading = false; + + /* Data loaded from the database */ + List players = []; + List games = []; + List groups = []; + + /* User selections */ + StatisticType? selectedType; + List selectedScope = []; + List selectedGames = []; + List selectedPlayers = []; + List selectedGroups = []; + Timeframe? selectedTimeframe; + + @override + void initState() { + loadAllData(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + var loc = AppLocalizations.of(context); + + return ScaffoldMessenger( + child: Scaffold( + appBar: AppBar(title: Text(loc.create_statistic)), + body: Stack( + alignment: AlignmentDirectional.center, + children: [ + SingleChildScrollView( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).padding.bottom + 80, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Classifier title + Padding( + padding: const EdgeInsetsGeometry.only(left: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + loc.create_statistic_classifier_title, + textAlign: TextAlign.start, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Text( + loc.create_statistic_classifier_subtitle, + textAlign: TextAlign.start, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 12, + ), + softWrap: true, + ), + ], + ), + ), + + // Classifier selection + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + child: CustomDropdown( + closedHeaderPadding: const EdgeInsets.symmetric( + vertical: 16, + horizontal: 16, + ), + listItemBuilder: + (context, item, isSelected, onItemSelect) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + translateStatisticTypeToString(item, context), + style: itemStyle, + ), + if (isSelected) + const Icon( + Icons.check, + color: CustomTheme.textColor, + ), + ], + ), + headerBuilder: (context, selectedType, enabled) => Text( + translateStatisticTypeToString(selectedType, context), + style: headerStyle, + ), + hintText: loc.select_a_classifier, + items: StatisticType.values, + decoration: decoration, + onChanged: (value) { + setState(() { + selectedType = value; + }); + }, + ), + ), + + const SizedBox(height: 10), + + // Scope title + Padding( + padding: const EdgeInsetsGeometry.only(left: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + loc.create_statistic_scope_title, + textAlign: TextAlign.start, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Text( + loc.create_statistic_scope_subtitle, + textAlign: TextAlign.start, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 12, + ), + ), + ], + ), + ), + + // Scope selection + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + child: CustomDropdown.multiSelect( + closedHeaderPadding: const EdgeInsets.symmetric( + vertical: 16, + horizontal: 16, + ), + hintText: loc.select_a_scope, + items: StatisticScope.values, + decoration: decoration, + listItemBuilder: + (context, scope, isSelected, onItemSelect) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + translateScopeToString(scope, context), + style: itemStyle, + ), + if (isSelected) + const Icon( + Icons.check, + color: CustomTheme.textColor, + ), + ], + ), + headerListBuilder: (context, selectedItems, enabled) => + Text( + selectedItems + .map((s) => translateScopeToString(s, context)) + .join(', '), + style: headerStyle, + overflow: TextOverflow.ellipsis, + ), + onListChanged: (List values) { + setState(() { + selectedScope = values; + }); + }, + ), + ), + + if (selectedScope.contains(StatisticScope.selectedGames)) ...[ + const SizedBox(height: 10), + + // games title + Padding( + padding: const EdgeInsetsGeometry.only(left: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + loc.create_statistic_games_title, + textAlign: TextAlign.start, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Text( + loc.create_statistic_games_subtitle, + textAlign: TextAlign.start, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 12, + ), + ), + ], + ), + ), + + // game selection + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + child: CustomDropdown.multiSelect( + enabled: !isLoading, + disabledDecoration: disabledDecoration, + closedHeaderPadding: const EdgeInsets.symmetric( + vertical: 16, + horizontal: 16, + ), + hintText: isLoading ? loc.loading : loc.select_a_game, + items: games, + decoration: decoration, + listItemBuilder: + (context, item, isSelected, onItemSelect) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // Name + Text(item.name, style: itemStyle), + const SizedBox(width: 12), + + // Ruleset + Text( + translateRulesetToString( + item.ruleset, + context, + ), + style: hintStyle.copyWith(fontSize: 12), + ), + ], + ), + + // Check icon + if (isSelected) + const Icon( + Icons.check, + color: CustomTheme.textColor, + ), + ], + ), + headerListBuilder: (context, selectedItems, enabled) => + Text( + selectedItems.map((g) => g.name).join(', '), + style: const TextStyle( + color: CustomTheme.textColor, + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, + ), + onListChanged: (List values) { + setState(() { + selectedGames = values; + }); + }, + ), + ), + ], + + if (selectedScope.contains( + StatisticScope.selectedGroups, + )) ...[ + const SizedBox(height: 10), + + // groups title + Padding( + padding: const EdgeInsetsGeometry.only(left: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + loc.create_statistic_groups_title, + textAlign: TextAlign.start, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Text( + loc.create_statistic_groups_subtitle, + textAlign: TextAlign.start, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 12, + ), + ), + ], + ), + ), + + // groups selection + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + child: CustomDropdown.multiSelect( + enabled: !isLoading, + disabledDecoration: disabledDecoration, + closedHeaderPadding: const EdgeInsets.symmetric( + vertical: 16, + horizontal: 16, + ), + hintText: isLoading ? loc.loading : loc.select_a_group, + items: groups, + decoration: decoration, + listItemBuilder: + (context, item, isSelected, onItemSelect) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Name + Text(item.name, style: itemStyle), + const SizedBox(width: 12), + + // Ruleset + Text( + ' ${item.members.length.toString()} ${loc.members}', + style: hintStyle.copyWith(fontSize: 12), + ), + ], + ), + if (isSelected) + const Icon( + Icons.check, + color: CustomTheme.textColor, + ), + ], + ), + headerListBuilder: (context, selectedItems, enabled) => + Text( + selectedItems.map((g) => g.name).join(', '), + style: headerStyle, + overflow: TextOverflow.ellipsis, + ), + onListChanged: (List groups) { + setState(() { + selectedGroups = groups; + }); + }, + ), + ), + ], + + if (selectedScope.contains(StatisticScope.timeframe)) ...[ + const SizedBox(height: 10), + + // timeframe title + Padding( + padding: const EdgeInsetsGeometry.only(left: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + loc.create_statistic_timeframe_title, + textAlign: TextAlign.start, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Text( + loc.create_statistic_timeframe_subtitle, + textAlign: TextAlign.start, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 12, + ), + ), + ], + ), + ), + + // groups selection + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + child: CustomDropdown( + enabled: !isLoading, + excludeSelected: false, + disabledDecoration: disabledDecoration, + closedHeaderPadding: const EdgeInsets.symmetric( + vertical: 16, + horizontal: 16, + ), + hintText: isLoading + ? loc.loading + : loc.select_a_timeframe, + items: Timeframe.values, + decoration: decoration, + listItemBuilder: + (context, timeframe, isSelected, onItemSelect) => + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + translateTimeframeToString( + timeframe, + context, + ), + style: itemStyle, + ), + if (isSelected) + const Icon( + Icons.check, + color: CustomTheme.textColor, + ), + ], + ), + headerBuilder: (context, selectedTimeframe, enabled) => + Text( + translateTimeframeToString( + selectedTimeframe, + context, + ), + style: headerStyle, + overflow: TextOverflow.ellipsis, + ), + onChanged: (Timeframe? timeframe) { + setState(() { + selectedTimeframe = timeframe; + }); + }, + ), + ), + ], + ], + ), + ), + + // Create statistic button + Positioned( + bottom: MediaQuery.of(context).padding.bottom, + child: AnimatedDialogButton( + buttonConstraints: const BoxConstraints(minWidth: 350), + buttonText: loc.create_statistic, + onPressed: selectedType != null && selectedScope.isNotEmpty + ? () => submitStatistic() + : null, + ), + ), + ], + ), + ), + ); + } + + CustomDropdownDecoration get decoration => CustomDropdownDecoration( + listItemDecoration: const ListItemDecoration( + selectedIconBorder: BorderSide(color: CustomTheme.primaryColor, width: 1), + selectedIconColor: CustomTheme.primaryColor, + highlightColor: CustomTheme.secondaryColor, + splashColor: Colors.transparent, + selectedColor: CustomTheme.onBoxColor, + ), + listItemStyle: itemStyle, + headerStyle: headerStyle, + hintStyle: hintStyle, + closedFillColor: CustomTheme.boxColor, + closedBorder: Border.all(color: CustomTheme.boxBorderColor, width: 1), + expandedFillColor: CustomTheme.boxColor, + expandedBorder: Border.all(color: CustomTheme.boxBorderColor, width: 1), + ); + + CustomDropdownDisabledDecoration get disabledDecoration => + CustomDropdownDisabledDecoration( + fillColor: CustomTheme.boxColor.withAlpha(125), + border: Border.all( + color: CustomTheme.boxBorderColor.withAlpha(125), + width: 1, + ), + headerStyle: disabledHeaderStyle, + hintStyle: disabledHintStyle, + ); + + TextStyle get headerStyle => const TextStyle( + color: CustomTheme.textColor, + fontSize: 14, + fontWeight: FontWeight.bold, + ); + + TextStyle get itemStyle => + const TextStyle(color: CustomTheme.textColor, fontSize: 14); + + TextStyle get hintStyle => + const TextStyle(color: CustomTheme.hintColor, fontSize: 14); + + TextStyle get disabledHeaderStyle => const TextStyle( + color: CustomTheme.hintColor, + fontSize: 14, + fontWeight: FontWeight.bold, + ); + + TextStyle get disabledHintStyle => + const TextStyle(color: CustomTheme.hintColor, fontSize: 14); + + Future loadAllData() async { + isLoading = true; + final db = Provider.of(context, listen: false); + + Future.wait([ + db.playerDao.getAllPlayers(), + db.groupDao.getAllGroups(), + db.gameDao.getAllGames(), + Future.delayed(Constants.MINIMUM_SKELETON_DURATION), + ]) + .then((results) async { + players = results[0]; + groups = results[1]; + games = results[2]; + isLoading = false; + }) + .catchError((error) { + print('Error loading data: $error'); + }); + } + + void submitStatistic() { + final newStatistic = Statistic( + type: selectedType!, + scopes: selectedScope, + timeframe: selectedTimeframe, + selectedGroups: selectedGroups, + selectedGames: selectedGames, + ); + // final db = Provider.of(context, listen: false); + // db.statisticDao.addStatistic(newStatistic); + Navigator.of(context).pop(newStatistic); + } +} + +String translateTimeframeToString(Timeframe timeframe, BuildContext context) { + final loc = AppLocalizations.of(context); + switch (timeframe) { + case Timeframe.last7Days: + return loc.timeframe_last_7_days; + case Timeframe.last30Days: + return loc.timeframe_last_30_days; + case Timeframe.last90Days: + return loc.timeframe_last_90_days; + case Timeframe.last180Days: + return loc.timeframe_last_180_days; + case Timeframe.lastYear: + return loc.timeframe_last_year; + case Timeframe.allTime: + return loc.timeframe_all_time; + } +} + +String translateScopeToString(StatisticScope scope, BuildContext context) { + final loc = AppLocalizations.of(context); + switch (scope) { + case StatisticScope.allPlayers: + return loc.statistic_scope_all_players; + case StatisticScope.selectedGroups: + return loc.statistic_scope_selected_groups; + case StatisticScope.selectedGames: + return loc.statistic_scope_selected_games; + case StatisticScope.timeframe: + return loc.statistic_scope_timeframe; + } +} + +String translateStatisticTypeToString( + StatisticType type, + BuildContext context, +) { + final loc = AppLocalizations.of(context); + switch (type) { + case StatisticType.totalMatches: + return loc.statistic_type_total_matches; + case StatisticType.totalWins: + return loc.statistic_type_total_wins; + case StatisticType.totalScore: + return loc.statistic_type_total_score; + case StatisticType.totalLosses: + return loc.statistic_type_total_losses; + case StatisticType.averageScore: + return loc.statistic_type_average_score; + case StatisticType.bestScore: + return loc.statistic_type_best_score; + case StatisticType.worstScore: + return loc.statistic_type_worst_score; + case StatisticType.winrate: + return loc.statistic_type_winrate; + } +} diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view/statistics_view.dart similarity index 62% rename from lib/presentation/views/main_menu/statistics_view.dart rename to lib/presentation/views/main_menu/statistics_view/statistics_view.dart index 8659a2e..3470a12 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view/statistics_view.dart @@ -1,11 +1,14 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:tallee/core/adaptive_page_route.dart'; import 'package:tallee/core/constants.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.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/widgets/app_skeleton.dart'; +import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/tiles/quick_info_tile.dart'; import 'package:tallee/presentation/widgets/tiles/statistics_tile.dart'; import 'package:tallee/presentation/widgets/top_centered_message.dart'; @@ -47,85 +50,107 @@ class _StatisticsViewState extends State { final loc = AppLocalizations.of(context); return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { - return SingleChildScrollView( - child: AppSkeleton( - enabled: isLoading, - fixLayoutBuilder: true, - child: ConstrainedBox( - constraints: BoxConstraints(minWidth: constraints.maxWidth), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, + return Stack( + alignment: AlignmentDirectional.center, + children: [ + SingleChildScrollView( + child: AppSkeleton( + enabled: isLoading, + fixLayoutBuilder: true, + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: constraints.maxWidth), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - QuickInfoTile( - width: constraints.maxWidth * 0.45, - height: constraints.maxHeight * 0.13, - title: loc.matches, - icon: Icons.groups_rounded, - value: matchCount, + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QuickInfoTile( + width: constraints.maxWidth * 0.45, + height: constraints.maxHeight * 0.13, + title: loc.matches, + icon: Icons.groups_rounded, + value: matchCount, + ), + SizedBox(width: constraints.maxWidth * 0.05), + QuickInfoTile( + width: constraints.maxWidth * 0.45, + height: constraints.maxHeight * 0.13, + title: loc.groups, + icon: Icons.groups_rounded, + value: groupCount, + ), + ], ), - SizedBox(width: constraints.maxWidth * 0.05), - QuickInfoTile( - width: constraints.maxWidth * 0.45, - height: constraints.maxHeight * 0.13, - title: loc.groups, - icon: Icons.groups_rounded, - value: groupCount, + SizedBox(height: constraints.maxHeight * 0.02), + Visibility( + visible: + winCounts.isEmpty && + matchCounts.isEmpty && + winRates.isEmpty, + replacement: Column( + children: [ + StatisticsTile( + icon: Icons.sports_score, + title: loc.wins, + width: constraints.maxWidth * 0.95, + values: winCounts, + itemCount: 3, + barColor: Colors.green, + ), + SizedBox(height: constraints.maxHeight * 0.02), + StatisticsTile( + icon: Icons.percent, + title: loc.winrate, + width: constraints.maxWidth * 0.95, + values: winRates, + itemCount: 5, + barColor: Colors.orange[700]!, + ), + SizedBox(height: constraints.maxHeight * 0.02), + StatisticsTile( + icon: Icons.casino, + title: loc.amount_of_matches, + width: constraints.maxWidth * 0.95, + values: matchCounts, + itemCount: 10, + barColor: Colors.blue, + ), + ], + ), + child: TopCenteredMessage( + icon: Icons.info, + title: loc.info, + message: AppLocalizations.of( + context, + ).no_statistics_available, + ), ), + SizedBox(height: MediaQuery.paddingOf(context).bottom), ], ), - SizedBox(height: constraints.maxHeight * 0.02), - Visibility( - visible: - winCounts.isEmpty && - matchCounts.isEmpty && - winRates.isEmpty, - replacement: Column( - children: [ - StatisticsTile( - icon: Icons.sports_score, - title: loc.wins, - width: constraints.maxWidth * 0.95, - values: winCounts, - itemCount: 3, - barColor: Colors.green, - ), - SizedBox(height: constraints.maxHeight * 0.02), - StatisticsTile( - icon: Icons.percent, - title: loc.winrate, - width: constraints.maxWidth * 0.95, - values: winRates, - itemCount: 5, - barColor: Colors.orange[700]!, - ), - SizedBox(height: constraints.maxHeight * 0.02), - StatisticsTile( - icon: Icons.casino, - title: loc.amount_of_matches, - width: constraints.maxWidth * 0.95, - values: matchCounts, - itemCount: 10, - barColor: Colors.blue, - ), - ], - ), - child: TopCenteredMessage( - icon: Icons.info, - title: loc.info, - message: AppLocalizations.of( - context, - ).no_statistics_available, - ), - ), - SizedBox(height: MediaQuery.paddingOf(context).bottom), - ], + ), ), ), - ), + Positioned( + bottom: MediaQuery.paddingOf(context).bottom + 20, + child: MainMenuButton( + text: loc.create_statistic, + icon: Icons.bar_chart, + onPressed: () { + Navigator.push( + context, + adaptivePageRoute( + builder: (context) => CreateStatisticView( + onStatisticCreated: loadStatisticData, + ), + ), + ); + }, + ), + ), + ], ); }, ); diff --git a/pubspec.lock b/pubspec.lock index 96b2cc5..fc30e57 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "10.0.1" + animated_custom_dropdown: + dependency: "direct main" + description: + name: animated_custom_dropdown + sha256: "5a72dc209041bb53f6c7164bc2e366552d5197cdb032b1c9b2c36e3013024486" + url: "https://pub.dev" + source: hosted + version: "3.1.1" arb_utils: dependency: "direct dev" description: @@ -353,6 +361,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.8" + dropdown_flutter: + dependency: "direct main" + description: + name: dropdown_flutter + sha256: "5ae3d05d768d0bb6030ff735e6b4b93f7b29be3cf3bec7c86cd4f444c8f067ff" + url: "https://pub.dev" + source: hosted + version: "1.0.3" equatable: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3d8d99f..8ca8550 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,11 +7,13 @@ environment: sdk: ^3.8.1 dependencies: + animated_custom_dropdown: ^3.1.1 clock: ^1.1.2 collection: ^1.19.1 cupertino_icons: ^1.0.6 drift: ^2.27.0 drift_flutter: ^0.2.4 + dropdown_flutter: ^1.0.3 file_picker: ^11.0.2 file_saver: ^0.3.1 flutter: