feat: create statistics view
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 48s
Pull Request Pipeline / test (pull_request) Successful in 49s
Pull Request Pipeline / localizations (pull_request) Successful in 27s

This commit is contained in:
2026-05-24 01:26:08 +02:00
parent 57ebea1eb7
commit 134f77c5a3
12 changed files with 1296 additions and 73 deletions

View File

@@ -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,
}

View File

@@ -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<StatisticScope> scopes;
final Timeframe? timeframe;
final List<Group>? selectedGroups;
final List<Game>? selectedGames;
Statistic({
required this.type,
required this.scopes,
this.timeframe,
this.selectedGroups,
this.selectedGames,
});
}

View File

@@ -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)",

View File

@@ -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)",

View File

@@ -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:

View File

@@ -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';

View File

@@ -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';

View File

@@ -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';

View File

@@ -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<CreateStatisticView> createState() => _CreateStatisticViewState();
}
class _CreateStatisticViewState extends State<CreateStatisticView> {
bool isLoading = false;
/* Data loaded from the database */
List<Player> players = [];
List<Game> games = [];
List<Group> groups = [];
/* User selections */
StatisticType? selectedType;
List<StatisticScope> selectedScope = [];
List<Game> selectedGames = [];
List<Player> selectedPlayers = [];
List<Group> 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<StatisticType>(
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<StatisticScope>.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<StatisticScope> 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<Game>.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<Game> 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<Group>.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<Group> 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<Timeframe>(
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<void> loadAllData() async {
isLoading = true;
final db = Provider.of<AppDatabase>(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<AppDatabase>(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;
}
}

View File

@@ -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<StatisticsView> {
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,
),
),
);
},
),
),
],
);
},
);

View File

@@ -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:

View File

@@ -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: