MVP-Refactoring #139

Merged
flixcoo merged 20 commits from refactoring/68-mvp-refactoring into development 2026-01-08 20:24:01 +00:00
39 changed files with 1093 additions and 833 deletions

View File

@@ -1,2 +1,6 @@
/// Minimum duration of all app skeletons class Constants {
Duration minimumSkeletonDuration = const Duration(milliseconds: 250); Constants._(); // Private constructor to prevent instantiation
/// Minimum duration of all app skeletons
static Duration minimumSkeletonDuration = const Duration(milliseconds: 250);
}

View File

@@ -1,35 +1,59 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class CustomTheme { class CustomTheme {
CustomTheme._(); // Private constructor to prevent instantiation
// ==================== Colors ====================
static Color primaryColor = const Color(0xFF7505E4); static Color primaryColor = const Color(0xFF7505E4);
static Color secondaryColor = const Color(0xFFAFA2FF); static Color secondaryColor = const Color(0xFFAFA2FF);
static Color backgroundColor = const Color(0xFF0B0B0B); static Color backgroundColor = const Color(0xFF0B0B0B);
static Color boxColor = const Color(0xFF101010); static Color boxColor = const Color(0xFF101010);
static Color onBoxColor = const Color(0xFF181818); static Color onBoxColor = const Color(0xFF181818);
static Color boxBorder = const Color(0xFF272727); static Color boxBorder = const Color(0xFF272727);
static const Color textColor = Colors.white;
// ==================== Border Radius ====================
static const double standardBorderRadius = 12.0;
static BorderRadius get standardBorderRadiusAll =>
BorderRadius.circular(standardBorderRadius);
// ==================== Padding & Margins ====================
static const EdgeInsets standardMargin = EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
);
static const EdgeInsets tileMargin = EdgeInsets.symmetric(
horizontal: 12,
vertical: 5,
);
// ==================== Decorations ====================
static BoxDecoration standardBoxDecoration = BoxDecoration( static BoxDecoration standardBoxDecoration = BoxDecoration(
color: boxColor, color: boxColor,
border: Border.all(color: boxBorder), border: Border.all(color: boxBorder),
borderRadius: BorderRadius.circular(12), borderRadius: standardBorderRadiusAll,
); );
static BoxDecoration highlightedBoxDecoration = BoxDecoration( static BoxDecoration highlightedBoxDecoration = BoxDecoration(
color: boxColor, color: boxColor,
border: Border.all(color: primaryColor), border: Border.all(color: primaryColor),
borderRadius: BorderRadius.circular(12), borderRadius: standardBorderRadiusAll,
boxShadow: [BoxShadow(color: primaryColor.withAlpha(120), blurRadius: 12)], boxShadow: [BoxShadow(color: primaryColor.withAlpha(120), blurRadius: 12)],
); );
// ==================== App Bar Theme ====================
static AppBarTheme appBarTheme = AppBarTheme( static AppBarTheme appBarTheme = AppBarTheme(
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
foregroundColor: Colors.white, foregroundColor: textColor,
elevation: 0, elevation: 0,
scrolledUnderElevation: 0,
centerTitle: true,
titleTextStyle: const TextStyle( titleTextStyle: const TextStyle(
color: Colors.white, color: textColor,
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
overflow: TextOverflow.ellipsis,
), ),
iconTheme: const IconThemeData(color: Colors.white), iconTheme: const IconThemeData(color: textColor),
); );
} }

View File

@@ -2,6 +2,9 @@ import 'package:flutter/material.dart';
import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/l10n/generated/app_localizations.dart';
/// Button types used for styling the [CustomWidthButton] /// Button types used for styling the [CustomWidthButton]
/// - [ButtonType.primary]: Primary button style.
/// - [ButtonType.secondary]: Secondary button style.
/// - [ButtonType.tertiary]: Tertiary button style.
enum ButtonType { primary, secondary, tertiary } enum ButtonType { primary, secondary, tertiary }
/// Result types for import operations in the [SettingsView] /// Result types for import operations in the [SettingsView]

View File

@@ -1,17 +1,17 @@
{ {
"@@locale": "de", "@@locale": "de",
"all_players": "Alle Spieler:innen:", "all_players": "Alle Spieler:innen",
"all_players_selected": "Alle Spieler:innen ausgewählt", "all_players_selected": "Alle Spieler:innen ausgewählt",
"amount_of_matches": "Anzahl der Matches", "amount_of_matches": "Anzahl der Spiele",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"choose_game": "Spielvorlage wählen", "choose_game": "Spielvorlage wählen",
"choose_group": "Gruppe wählen", "choose_group": "Gruppe wählen",
"choose_ruleset": "Regelwerk wählen", "choose_ruleset": "Regelwerk wählen",
"could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden", "could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden",
"create_group": "Gruppe erstellen", "create_group": "Gruppe erstellen",
"create_match": "Match erstellen", "create_match": "Spiel erstellen",
"create_new_group": "Neue Gruppe erstellen", "create_new_group": "Neue Gruppe erstellen",
"create_new_match": "Neues Match erstellen", "create_new_match": "Neues Spiel erstellen",
"data_successfully_deleted": "Daten erfolgreich gelöscht", "data_successfully_deleted": "Daten erfolgreich gelöscht",
"data_successfully_exported": "Daten erfolgreich exportiert", "data_successfully_exported": "Daten erfolgreich exportiert",
"data_successfully_imported": "Daten erfolgreich importiert", "data_successfully_imported": "Daten erfolgreich importiert",
@@ -34,28 +34,28 @@
"info": "Info", "info": "Info",
"invalid_schema": "Ungültiges Schema", "invalid_schema": "Ungültiges Schema",
"least_points": "Niedrigste Punkte", "least_points": "Niedrigste Punkte",
"match_in_progress": "Match läuft...", "match_in_progress": "Spiel läuft...",
"match_name": "Matchname", "match_name": "Spieltitel",
"matches": "Matches", "matches": "Spiele",
"menu": "Menü", "menu": "Menü",
"most_points": "Höchste Punkte", "most_points": "Höchste Punkte",
"no_data_available": "Keine Daten verfügbar", "no_data_available": "Keine Daten verfügbar",
"no_groups_created_yet": "Noch keine Gruppen erstellt", "no_groups_created_yet": "Noch keine Gruppen erstellt",
"no_matches_created_yet": "Noch keine Matches erstellt", "no_matches_created_yet": "Noch keine Spiele erstellt",
"no_players_created_yet": "Noch keine Spieler:in erstellt", "no_players_created_yet": "Noch keine Spieler:in erstellt",
"no_players_found_with_that_name": "Keine Spieler:in mit diesem Namen gefunden", "no_players_found_with_that_name": "Keine Spieler:in mit diesem Namen gefunden",
"no_players_selected": "Keine Spieler:in ausgewählt", "no_players_selected": "Keine Spieler:innen ausgewählt",
"no_recent_matches_available": "Keine letzten Matches verfügbar", "no_recent_matches_available": "Keine letzten Spiele verfügbar",
"no_second_match_available": "Kein zweites Match verfügbar", "no_second_match_available": "Kein zweites Spiel verfügbar",
"no_statistics_available": "Keine Statistiken verfügbar", "no_statistics_available": "Keine Statistiken verfügbar",
"none": "Kein", "none": "Kein",
"none_group": "Keine", "none_group": "Keine",
"not_available": "Nicht verfügbar", "not_available": "Nicht verfügbar",
"player_name": "Spieler:innenname", "player_name": "Spieler:innenname",
"players": "Spieler:in", "players": "Spieler:innen",
"players_count": "{count} Spieler", "players_count": "{count} Spieler",
"quick_create": "Schnellzugriff", "quick_create": "Schnellzugriff",
"recent_matches": "Letzte Matches", "recent_matches": "Letzte Spiele",
"ruleset": "Regelwerk", "ruleset": "Regelwerk",
"ruleset_least_points": "Umgekehrte Wertung: Der/die Spieler:in mit den wenigsten Punkten gewinnt.", "ruleset_least_points": "Umgekehrte Wertung: Der/die Spieler:in mit den wenigsten Punkten gewinnt.",
"ruleset_most_points": "Traditionelles Regelwerk: Der/die Spieler:in mit den meisten Punkten gewinnt.", "ruleset_most_points": "Traditionelles Regelwerk: Der/die Spieler:in mit den meisten Punkten gewinnt.",
@@ -64,7 +64,7 @@
"search_for_groups": "Nach Gruppen suchen", "search_for_groups": "Nach Gruppen suchen",
"search_for_players": "Nach Spieler:innen suchen", "search_for_players": "Nach Spieler:innen suchen",
"select_winner": "Gewinner:in wählen:", "select_winner": "Gewinner:in wählen:",
"selected_players": "Ausgewählte Spieler:in: {count}", "selected_players": "Ausgewählte Spieler:innen",
"settings": "Einstellungen", "settings": "Einstellungen",
"single_loser": "Ein:e Verlierer:in", "single_loser": "Ein:e Verlierer:in",
"single_winner": "Ein:e Gewinner:in", "single_winner": "Ein:e Gewinner:in",
@@ -73,11 +73,11 @@
"successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt", "successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt",
"there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht", "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", "this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden",
"today_at": "Heute um {time}", "today_at": "Heute um",
"undo": "Rückgängig", "undo": "Rückgängig",
"unknown_exception": "Unbekannter Fehler (siehe Konsole)", "unknown_exception": "Unbekannter Fehler (siehe Konsole)",
"winner": "Gewinner:in: {winnerName}", "winner": "Gewinner:in",
"winrate": "Siegquote", "winrate": "Siegquote",
"wins": "Siege", "wins": "Siege",
"yesterday_at": "Gestern um {time}" "yesterday_at": "Gestern um"
} }

View File

@@ -9,6 +9,9 @@
"@amount_of_matches": { "@amount_of_matches": {
"description": "Label for amount of matches statistic" "description": "Label for amount of matches statistic"
}, },
"@app_name": {
"description": "The name of the App"
},
"@cancel": { "@cancel": {
"description": "Cancel button text" "description": "Cancel button text"
}, },
@@ -22,13 +25,7 @@
"description": "Label for choosing a ruleset" "description": "Label for choosing a ruleset"
}, },
"@could_not_add_player": { "@could_not_add_player": {
"description": "Error message when adding a player fails", "description": "Error message when adding a player fails"
"placeholders": {
"playerName": {
"type": "String",
"example": "John"
}
}
}, },
"@create_group": { "@create_group": {
"description": "Button text to create a group" "description": "Button text to create a group"
@@ -86,9 +83,6 @@
"@game_name": { "@game_name": {
"description": "Placeholder for game name search" "description": "Placeholder for game name search"
}, },
"@game_tracker": {
"description": "App Name"
},
"@group": { "@group": {
"description": "Group label" "description": "Group label"
}, },
@@ -212,13 +206,7 @@
"description": "Label to select the winner" "description": "Label to select the winner"
}, },
"@selected_players": { "@selected_players": {
"description": "Shows the number of selected players", "description": "Shows the number of selected players"
"placeholders": {
"count": {
"type": "int",
"format": "compact"
}
}
}, },
"@settings": { "@settings": {
"description": "Settings label" "description": "Settings label"
@@ -251,13 +239,7 @@
"description": "Warning message for irreversible actions" "description": "Warning message for irreversible actions"
}, },
"@today_at": { "@today_at": {
"description": "Date format for today", "description": "Date format for today"
"placeholders": {
"time": {
"type": "String",
"example": "14:30"
}
}
}, },
"@undo": { "@undo": {
"description": "Undo button text" "description": "Undo button text"
@@ -275,22 +257,17 @@
"description": "Label for wins statistic" "description": "Label for wins statistic"
}, },
"@yesterday_at": { "@yesterday_at": {
"description": "Date format for yesterday", "description": "Date format for yesterday"
"placeholders": {
"time": {
"type": "String",
"example": "14:30"
}
}
}, },
"all_players": "All players:", "all_players": "All players",
"all_players_selected": "All players selected", "all_players_selected": "All players selected",
"amount_of_matches": "Amount of Matches", "amount_of_matches": "Amount of Matches",
"app_name": "Game Tracker",
"cancel": "Cancel", "cancel": "Cancel",
"choose_game": "Choose Game", "choose_game": "Choose Game",
"choose_group": "Choose Group", "choose_group": "Choose Group",
"choose_ruleset": "Choose Ruleset", "choose_ruleset": "Choose Ruleset",
"could_not_add_player": "Could not add player {playerName}", "could_not_add_player": "Could not add player",
"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",
@@ -308,7 +285,6 @@
"format_exception": "Format Exception (see console)", "format_exception": "Format Exception (see console)",
"game": "Game", "game": "Game",
"game_name": "Game Name", "game_name": "Game Name",
"game_tracker": "Game Tracker",
"group": "Group", "group": "Group",
"group_name": "Group name", "group_name": "Group name",
"groups": "Groups", "groups": "Groups",
@@ -348,7 +324,7 @@
"search_for_groups": "Search for groups", "search_for_groups": "Search for groups",
"search_for_players": "Search for players", "search_for_players": "Search for players",
"select_winner": "Select Winner:", "select_winner": "Select Winner:",
"selected_players": "Selected players: {count}", "selected_players": "Selected players",
"settings": "Settings", "settings": "Settings",
"single_loser": "Single Loser", "single_loser": "Single Loser",
"single_winner": "Single Winner", "single_winner": "Single Winner",
@@ -357,11 +333,11 @@
"successfully_added_player": "Successfully added player {playerName}", "successfully_added_player": "Successfully added player {playerName}",
"there_is_no_group_matching_your_search": "There is no group matching your search", "there_is_no_group_matching_your_search": "There is no group matching your search",
"this_cannot_be_undone": "This can't be undone", "this_cannot_be_undone": "This can't be undone",
"today_at": "Today at {time}", "today_at": "Today at",
"undo": "Undo", "undo": "Undo",
"unknown_exception": "Unknown Exception (see console)", "unknown_exception": "Unknown Exception (see console)",
"winner": "Winner", "winner": "Winner",
"winrate": "Winrate", "winrate": "Winrate",
"wins": "Wins", "wins": "Wins",
"yesterday_at": "Yesterday at {time}" "yesterday_at": "Yesterday at"
} }

View File

@@ -101,7 +101,7 @@ abstract class AppLocalizations {
/// Label for all players list /// Label for all players list
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'All players:'** /// **'All players'**
String get all_players; String get all_players;
/// Message when all players are added to selection /// Message when all players are added to selection
@@ -116,6 +116,12 @@ abstract class AppLocalizations {
/// **'Amount of Matches'** /// **'Amount of Matches'**
String get amount_of_matches; String get amount_of_matches;
/// The name of the App
///
/// In en, this message translates to:
/// **'Game Tracker'**
String get app_name;
/// Cancel button text /// Cancel button text
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -143,8 +149,8 @@ abstract class AppLocalizations {
/// Error message when adding a player fails /// Error message when adding a player fails
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Could not add player {playerName}'** /// **'Could not add player'**
String could_not_add_player(String playerName); String could_not_add_player(Object playerName);
/// Button text to create a group /// Button text to create a group
/// ///
@@ -248,12 +254,6 @@ abstract class AppLocalizations {
/// **'Game Name'** /// **'Game Name'**
String get game_name; String get game_name;
/// App Name
///
/// In en, this message translates to:
/// **'Game Tracker'**
String get game_tracker;
/// Group label /// Group label
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -491,8 +491,8 @@ abstract class AppLocalizations {
/// Shows the number of selected players /// Shows the number of selected players
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Selected players: {count}'** /// **'Selected players'**
String selected_players(int count); String get selected_players;
/// Settings label /// Settings label
/// ///
@@ -545,8 +545,8 @@ abstract class AppLocalizations {
/// Date format for today /// Date format for today
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Today at {time}'** /// **'Today at'**
String today_at(String time); String get today_at;
/// Undo button text /// Undo button text
/// ///
@@ -564,7 +564,7 @@ abstract class AppLocalizations {
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Winner'** /// **'Winner'**
String winner(Object winnerName); String get winner;
/// Label for winrate statistic /// Label for winrate statistic
/// ///
@@ -581,8 +581,8 @@ abstract class AppLocalizations {
/// Date format for yesterday /// Date format for yesterday
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Yesterday at {time}'** /// **'Yesterday at'**
String yesterday_at(String time); String get yesterday_at;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View File

@@ -9,13 +9,16 @@ class AppLocalizationsDe extends AppLocalizations {
AppLocalizationsDe([String locale = 'de']) : super(locale); AppLocalizationsDe([String locale = 'de']) : super(locale);
@override @override
String get all_players => 'Alle Spieler:innen:'; String get all_players => 'Alle Spieler:innen';
@override @override
String get all_players_selected => 'Alle Spieler:innen ausgewählt'; String get all_players_selected => 'Alle Spieler:innen ausgewählt';
@override @override
String get amount_of_matches => 'Anzahl der Matches'; String get amount_of_matches => 'Anzahl der Spiele';
@override
String get app_name => 'Game Tracker';
@override @override
String get cancel => 'Abbrechen'; String get cancel => 'Abbrechen';
@@ -30,7 +33,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get choose_ruleset => 'Regelwerk wählen'; String get choose_ruleset => 'Regelwerk wählen';
@override @override
String could_not_add_player(String playerName) { String could_not_add_player(Object playerName) {
return 'Spieler:in $playerName konnte nicht hinzugefügt werden'; return 'Spieler:in $playerName konnte nicht hinzugefügt werden';
} }
@@ -38,13 +41,13 @@ class AppLocalizationsDe extends AppLocalizations {
String get create_group => 'Gruppe erstellen'; String get create_group => 'Gruppe erstellen';
@override @override
String get create_match => 'Match erstellen'; String get create_match => 'Spiel erstellen';
@override @override
String get create_new_group => 'Neue Gruppe erstellen'; String get create_new_group => 'Neue Gruppe erstellen';
@override @override
String get create_new_match => 'Neues Match erstellen'; String get create_new_match => 'Neues Spiel erstellen';
@override @override
String get data_successfully_deleted => 'Daten erfolgreich gelöscht'; String get data_successfully_deleted => 'Daten erfolgreich gelöscht';
@@ -88,9 +91,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get game_name => 'Spielvorlagenname'; String get game_name => 'Spielvorlagenname';
@override
String get game_tracker => 'Game Tracker';
@override @override
String get group => 'Gruppe'; String get group => 'Gruppe';
@@ -119,13 +119,13 @@ class AppLocalizationsDe extends AppLocalizations {
String get least_points => 'Niedrigste Punkte'; String get least_points => 'Niedrigste Punkte';
@override @override
String get match_in_progress => 'Match läuft...'; String get match_in_progress => 'Spiel läuft...';
@override @override
String get match_name => 'Matchname'; String get match_name => 'Spieltitel';
@override @override
String get matches => 'Matches'; String get matches => 'Spiele';
@override @override
String get menu => 'Menü'; String get menu => 'Menü';
@@ -140,7 +140,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get no_groups_created_yet => 'Noch keine Gruppen erstellt'; String get no_groups_created_yet => 'Noch keine Gruppen erstellt';
@override @override
String get no_matches_created_yet => 'Noch keine Matches erstellt'; String get no_matches_created_yet => 'Noch keine Spiele erstellt';
@override @override
String get no_players_created_yet => 'Noch keine Spieler:in erstellt'; String get no_players_created_yet => 'Noch keine Spieler:in erstellt';
@@ -150,13 +150,13 @@ class AppLocalizationsDe extends AppLocalizations {
'Keine Spieler:in mit diesem Namen gefunden'; 'Keine Spieler:in mit diesem Namen gefunden';
@override @override
String get no_players_selected => 'Keine Spieler:in ausgewählt'; String get no_players_selected => 'Keine Spieler:innen ausgewählt';
@override @override
String get no_recent_matches_available => 'Keine letzten Matches verfügbar'; String get no_recent_matches_available => 'Keine letzten Spiele verfügbar';
@override @override
String get no_second_match_available => 'Kein zweites Match verfügbar'; String get no_second_match_available => 'Kein zweites Spiel verfügbar';
@override @override
String get no_statistics_available => 'Keine Statistiken verfügbar'; String get no_statistics_available => 'Keine Statistiken verfügbar';
@@ -174,7 +174,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get player_name => 'Spieler:innenname'; String get player_name => 'Spieler:innenname';
@override @override
String get players => 'Spieler:in'; String get players => 'Spieler:innen';
@override @override
String players_count(int count) { String players_count(int count) {
@@ -185,7 +185,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get quick_create => 'Schnellzugriff'; String get quick_create => 'Schnellzugriff';
@override @override
String get recent_matches => 'Letzte Matches'; String get recent_matches => 'Letzte Spiele';
@override @override
String get ruleset => 'Regelwerk'; String get ruleset => 'Regelwerk';
@@ -216,14 +216,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get select_winner => 'Gewinner:in wählen:'; String get select_winner => 'Gewinner:in wählen:';
@override @override
String selected_players(int count) { String get selected_players => 'Ausgewählte Spieler:innen';
final intl.NumberFormat countNumberFormat = intl.NumberFormat.compact(
locale: localeName,
);
final String countString = countNumberFormat.format(count);
return 'Ausgewählte Spieler:in: $countString';
}
@override @override
String get settings => 'Einstellungen'; String get settings => 'Einstellungen';
@@ -254,9 +247,7 @@ class AppLocalizationsDe extends AppLocalizations {
'Dies kann nicht rückgängig gemacht werden'; 'Dies kann nicht rückgängig gemacht werden';
@override @override
String today_at(String time) { String get today_at => 'Heute um';
return 'Heute um $time';
}
@override @override
String get undo => 'Rückgängig'; String get undo => 'Rückgängig';
@@ -265,9 +256,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get unknown_exception => 'Unbekannter Fehler (siehe Konsole)'; String get unknown_exception => 'Unbekannter Fehler (siehe Konsole)';
@override @override
String winner(Object winnerName) { String get winner => 'Gewinner:in';
return 'Gewinner:in: $winnerName';
}
@override @override
String get winrate => 'Siegquote'; String get winrate => 'Siegquote';
@@ -276,7 +265,5 @@ class AppLocalizationsDe extends AppLocalizations {
String get wins => 'Siege'; String get wins => 'Siege';
@override @override
String yesterday_at(String time) { String get yesterday_at => 'Gestern um';
return 'Gestern um $time';
}
} }

View File

@@ -9,7 +9,7 @@ class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale); AppLocalizationsEn([String locale = 'en']) : super(locale);
@override @override
String get all_players => 'All players:'; String get all_players => 'All players';
@override @override
String get all_players_selected => 'All players selected'; String get all_players_selected => 'All players selected';
@@ -17,6 +17,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get amount_of_matches => 'Amount of Matches'; String get amount_of_matches => 'Amount of Matches';
@override
String get app_name => 'Game Tracker';
@override @override
String get cancel => 'Cancel'; String get cancel => 'Cancel';
@@ -30,8 +33,8 @@ class AppLocalizationsEn extends AppLocalizations {
String get choose_ruleset => 'Choose Ruleset'; String get choose_ruleset => 'Choose Ruleset';
@override @override
String could_not_add_player(String playerName) { String could_not_add_player(Object playerName) {
return 'Could not add player $playerName'; return 'Could not add player';
} }
@override @override
@@ -88,9 +91,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get game_name => 'Game Name'; String get game_name => 'Game Name';
@override
String get game_tracker => 'Game Tracker';
@override @override
String get group => 'Group'; String get group => 'Group';
@@ -216,14 +216,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get select_winner => 'Select Winner:'; String get select_winner => 'Select Winner:';
@override @override
String selected_players(int count) { String get selected_players => 'Selected players';
final intl.NumberFormat countNumberFormat = intl.NumberFormat.compact(
locale: localeName,
);
final String countString = countNumberFormat.format(count);
return 'Selected players: $countString';
}
@override @override
String get settings => 'Settings'; String get settings => 'Settings';
@@ -253,9 +246,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get this_cannot_be_undone => 'This can\'t be undone'; String get this_cannot_be_undone => 'This can\'t be undone';
@override @override
String today_at(String time) { String get today_at => 'Today at';
return 'Today at $time';
}
@override @override
String get undo => 'Undo'; String get undo => 'Undo';
@@ -264,9 +255,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get unknown_exception => 'Unknown Exception (see console)'; String get unknown_exception => 'Unknown Exception (see console)';
@override @override
String winner(Object winnerName) { String get winner => 'Winner';
return 'Winner';
}
@override @override
String get winrate => 'Winrate'; String get winrate => 'Winrate';
@@ -275,7 +264,5 @@ class AppLocalizationsEn extends AppLocalizations {
String get wins => 'Wins'; String get wins => 'Wins';
@override @override
String yesterday_at(String time) { String get yesterday_at => 'Yesterday at';
return 'Yesterday at $time';
}
} }

View File

@@ -34,21 +34,17 @@ class GameTracker extends StatelessWidget {
); );
}, },
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
onGenerateTitle: (context) => AppLocalizations.of(context).game_tracker, onGenerateTitle: (context) => AppLocalizations.of(context).app_name,
darkTheme: ThemeData.dark(),
themeMode: ThemeMode.dark, // forces dark mode themeMode: ThemeMode.dark, // forces dark mode
theme: ThemeData( theme: ThemeData(
primaryColor: CustomTheme.primaryColor, primaryColor: CustomTheme.primaryColor,
scaffoldBackgroundColor: CustomTheme.backgroundColor, scaffoldBackgroundColor: CustomTheme.backgroundColor,
appBarTheme: CustomTheme.appBarTheme, appBarTheme: CustomTheme.appBarTheme,
colorScheme: ColorScheme.fromSeed( colorScheme: ColorScheme.fromSeed(
seedColor: CustomTheme.primaryColor, seedColor: CustomTheme.primaryColor,
brightness: Brightness.dark, brightness: Brightness.dark,
).copyWith(surface: CustomTheme.backgroundColor), ).copyWith(surface: CustomTheme.backgroundColor),
), ),
home: const CustomNavigationBar(), home: const CustomNavigationBar(),
); );
} }

View File

@@ -17,7 +17,10 @@ class CustomNavigationBar extends StatefulWidget {
class _CustomNavigationBarState extends State<CustomNavigationBar> class _CustomNavigationBarState extends State<CustomNavigationBar>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
/// Currently selected tab index
int currentIndex = 0; int currentIndex = 0;
/// Key count to force rebuild of tab views
int tabKeyCount = 0; int tabKeyCount = 0;
@override @override
@@ -119,12 +122,14 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
); );
} }
/// Handles tab tap events. Updates the current [index] state.
void onTabTapped(int index) { void onTabTapped(int index) {
setState(() { setState(() {
currentIndex = index; currentIndex = index;
}); });
} }
/// Returns the title of the current tab based on [currentIndex].
String _currentTabTitle(context) { String _currentTabTitle(context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
switch (currentIndex) { switch (currentIndex) {

View File

@@ -18,15 +18,17 @@ class CreateGroupView extends StatefulWidget {
} }
class _CreateGroupViewState extends State<CreateGroupView> { class _CreateGroupViewState extends State<CreateGroupView> {
final _groupNameController = TextEditingController();
late final AppDatabase db; late final AppDatabase db;
sneeex marked this conversation as resolved
Review

warum für den rest comments aber nicht für db?

warum für den rest comments aber nicht für db?
Review

Habe Datenbank und isLoading nicht kommentiert, weil die so allgegenwärtig sind, dass die eigentlich keinen kommentar brauchen. Soll ich einen hinzufügen?

Habe Datenbank und `isLoading` nicht kommentiert, weil die so allgegenwärtig sind, dass die eigentlich keinen kommentar brauchen. Soll ich einen hinzufügen?
/// Controller for the group name input field
final _groupNameController = TextEditingController();
/// List of currently selected players
List<Player> selectedPlayers = []; List<Player> selectedPlayers = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
db = Provider.of<AppDatabase>(context, listen: false); db = Provider.of<AppDatabase>(context, listen: false);
_groupNameController.addListener(() { _groupNameController.addListener(() {
setState(() {}); setState(() {});
@@ -44,27 +46,16 @@ class _CreateGroupViewState extends State<CreateGroupView> {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
return Scaffold( return Scaffold(
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar( appBar: AppBar(title: Text(loc.create_new_group)),
sneeex marked this conversation as resolved
Review

schatz irgendwas passt hier nicht:
grafik.png

warum hast du alles weggelassen außer den title??? das ganze design ist anders jetzt und kacke auch

schatz irgendwas passt hier nicht: ![grafik.png](/attachments/2e570ad8-7b7b-4551-bb0c-576a4704f5f2) warum hast du alles weggelassen außer den title??? das ganze design ist anders jetzt und kacke auch
backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0,
title: Text(
loc.create_new_group,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
centerTitle: true,
),
body: SafeArea( body: SafeArea(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
Container( Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), margin: CustomTheme.standardMargin,
child: TextInputField( child: TextInputField(
controller: _groupNameController, controller: _groupNameController,
hintText: loc.group_name, hintText: loc.group_name,
onChanged: (value) {
setState(() {});
},
), ),
), ),
Expanded( Expanded(
@@ -109,7 +100,6 @@ class _CreateGroupViewState extends State<CreateGroupView> {
), ),
); );
} }
setState(() {});
}, },
), ),
const SizedBox(height: 20), const SizedBox(height: 20),

View File

@@ -21,7 +21,11 @@ class GroupsView extends StatefulWidget {
class _GroupsViewState extends State<GroupsView> { class _GroupsViewState extends State<GroupsView> {
late final AppDatabase db; late final AppDatabase db;
flixcoo marked this conversation as resolved
Review

hier ebenfalls kein comment?

hier ebenfalls kein comment?
/// Loaded groups from the database
late List<Group> loadedGroups; late List<Group> loadedGroups;
/// Loading state
bool isLoading = true; bool isLoading = true;
List<Group> groups = List.filled( List<Group> groups = List.filled(
@@ -101,7 +105,7 @@ class _GroupsViewState extends State<GroupsView> {
void loadGroups() { void loadGroups() {
Future.wait([ Future.wait([
db.groupDao.getAllGroups(), db.groupDao.getAllGroups(),
Future.delayed(minimumSkeletonDuration), Future.delayed(Constants.minimumSkeletonDuration),
]).then((results) { ]).then((results) {
loadedGroups = results[0] as List<Group>; loadedGroups = results[0] as List<Group>;
setState(() { setState(() {

View File

@@ -21,9 +21,17 @@ class HomeView extends StatefulWidget {
class _HomeViewState extends State<HomeView> { class _HomeViewState extends State<HomeView> {
bool isLoading = true; bool isLoading = true;
flixcoo marked this conversation as resolved
Review

hier ebenfalls kein comment?

hier ebenfalls kein comment?
/// Amount of matches in the database
int matchCount = 0; int matchCount = 0;
/// Amount of groups in the database
int groupCount = 0; int groupCount = 0;
/// Loaded recent matches from the database
List<Match> loadedRecentMatches = []; List<Match> loadedRecentMatches = [];
/// Recent matches to display, initially filled with skeleton matches
List<Match> recentMatches = List.filled( List<Match> recentMatches = List.filled(
2, 2,
Match( Match(
@@ -42,32 +50,7 @@ class _HomeViewState extends State<HomeView> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final db = Provider.of<AppDatabase>(context, listen: false); loadHomeViewData();
Future.wait([
db.matchDao.getMatchCount(),
db.groupDao.getGroupCount(),
db.matchDao.getAllMatches(),
Future.delayed(minimumSkeletonDuration),
]).then((results) {
matchCount = results[0] as int;
groupCount = results[1] as int;
loadedRecentMatches = results[2] as List<Match>;
recentMatches =
(loadedRecentMatches
..sort((a, b) => b.createdAt.compareTo(a.createdAt)))
.take(2)
.toList();
if (loadedRecentMatches.length < 2) {
recentMatches.add(
Match(name: 'Dummy Match', winner: null, group: null, players: null),
);
}
if (mounted) {
setState(() {
isLoading = false;
});
}
});
} }
@override @override
@@ -230,6 +213,40 @@ class _HomeViewState extends State<HomeView> {
); );
} }
/// Loads the data for the HomeView from the database.
/// This includes the match count, group count, and recent matches.
void loadHomeViewData() {
final db = Provider.of<AppDatabase>(context, listen: false);
Future.wait([
db.matchDao.getMatchCount(),
db.groupDao.getGroupCount(),
db.matchDao.getAllMatches(),
Future.delayed(Constants.minimumSkeletonDuration),
]).then((results) {
matchCount = results[0] as int;
groupCount = results[1] as int;
loadedRecentMatches = results[2] as List<Match>;
recentMatches =
(loadedRecentMatches
..sort((a, b) => b.createdAt.compareTo(a.createdAt)))
.take(2)
.toList();
if (loadedRecentMatches.length < 2) {
recentMatches.add(
Match(name: 'Dummy Match', winner: null, group: null, players: null),
);
}
if (mounted) {
setState(() {
isLoading = false;
});
}
});
}
/// Generates a text representation of the players in the match.
/// If the match has a group, it returns the group name and the number of additional players.
/// If there is no group, it returns the count of players.
String _getPlayerText(Match game, context) { String _getPlayerText(Match game, context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
if (game.group == null) { if (game.group == null) {

View File

@@ -20,10 +20,12 @@ class ChooseGameView extends StatefulWidget {
} }
class _ChooseGameViewState extends State<ChooseGameView> { class _ChooseGameViewState extends State<ChooseGameView> {
late int selectedGameIndex; /// Controller for the search bar
final TextEditingController searchBarController = TextEditingController(); final TextEditingController searchBarController = TextEditingController();
/// Currently selected game index
late int selectedGameIndex;
@override @override
void initState() { void initState() {
selectedGameIndex = widget.initialGameIndex; selectedGameIndex = widget.initialGameIndex;
@@ -36,19 +38,13 @@ class _ChooseGameViewState extends State<ChooseGameView> {
return Scaffold( return Scaffold(
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar( appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0,
leading: IconButton( leading: IconButton(
flixcoo marked this conversation as resolved
Review

auch hier mit dem design weird alles aufeinmal anders wie sonst
grafik.png

auch hier mit dem design weird alles aufeinmal anders wie sonst ![grafik.png](/attachments/93727713-90f7-42d7-ae20-1444d8709b44)
icon: const Icon(Icons.arrow_back_ios), icon: const Icon(Icons.arrow_back_ios),
onPressed: () { onPressed: () {
Navigator.of(context).pop(selectedGameIndex); Navigator.of(context).pop(selectedGameIndex);
}, },
), ),
title: Text( title: Text(loc.choose_game),
loc.choose_game,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
centerTitle: true,
), ),
body: PopScope( body: PopScope(
// This fixes that the Android Back Gesture didn't return the // This fixes that the Android Back Gesture didn't return the

View File

@@ -38,8 +38,6 @@ class _ChooseGroupViewState extends State<ChooseGroupView> {
return Scaffold( return Scaffold(
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar( appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0,
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back_ios), icon: const Icon(Icons.arrow_back_ios),
onPressed: () { onPressed: () {
@@ -52,11 +50,7 @@ class _ChooseGroupViewState extends State<ChooseGroupView> {
); );
}, },
), ),
title: Text( title: Text(loc.choose_group),
loc.choose_group,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
centerTitle: true,
), ),
flixcoo marked this conversation as resolved
Review

grafik.png

![grafik.png](/attachments/a9c6941a-0805-49e8-8585-7c0a56c3ce4d)
body: PopScope( body: PopScope(
// This fixes that the Android Back Gesture didn't return the // This fixes that the Android Back Gesture didn't return the
@@ -136,8 +130,7 @@ class _ChooseGroupViewState extends State<ChooseGroupView> {
); );
} }
/// Filters the groups based on the search query. /// Filters the groups based on the search [query].
sneeex marked this conversation as resolved
Review

warum query in klammern? entweder ganz oder garnicht aber wat soll dat hier

warum query in klammern? entweder ganz oder garnicht aber wat soll dat hier
Review

Weil das der Input Parameter der Funktion ist

Weil das der Input Parameter der Funktion ist
/// TODO: Maybe implement also targetting player names?
void filterGroups(String query) { void filterGroups(String query) {
setState(() { setState(() {
if (query.isEmpty) { if (query.isEmpty) {

View File

@@ -19,6 +19,7 @@ class ChooseRulesetView extends StatefulWidget {
} }
class _ChooseRulesetViewState extends State<ChooseRulesetView> { class _ChooseRulesetViewState extends State<ChooseRulesetView> {
/// Currently selected ruleset index
late int selectedRulesetIndex; late int selectedRulesetIndex;
@override @override
@@ -36,8 +37,6 @@ class _ChooseRulesetViewState extends State<ChooseRulesetView> {
child: Scaffold( child: Scaffold(
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar( appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0,
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back_ios), icon: const Icon(Icons.arrow_back_ios),
onPressed: () { onPressed: () {
flixcoo marked this conversation as resolved
Review

grafik.png

![grafik.png](/attachments/357e0556-4575-4f60-b812-dc7b582bf8b6)
@@ -48,11 +47,7 @@ class _ChooseRulesetViewState extends State<ChooseRulesetView> {
); );
}, },
), ),
title: Text( title: Text(loc.choose_ruleset),
loc.choose_ruleset,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
centerTitle: true,
), ),
body: PopScope( body: PopScope(
// This fixes that the Android Back Gesture didn't return the // This fixes that the Android Back Gesture didn't return the

View File

@@ -26,14 +26,13 @@ class CreateMatchView extends StatefulWidget {
} }
class _CreateMatchViewState extends State<CreateMatchView> { class _CreateMatchViewState extends State<CreateMatchView> {
/// Reference to the app database
late final AppDatabase db; late final AppDatabase db;
/// Controller for the match name input field /// Controller for the match name input field
final TextEditingController _matchNameController = TextEditingController(); final TextEditingController _matchNameController = TextEditingController();
/// Hint text for the match name input field /// Hint text for the match name input field
String hintText = 'Match Name'; String? hintText;
/// List of all groups from the database /// List of all groups from the database
List<Group> groupsList = []; List<Group> groupsList = [];
@@ -68,6 +67,9 @@ class _CreateMatchViewState extends State<CreateMatchView> {
/// The currently selected players /// The currently selected players
List<Player>? selectedPlayers; List<Player>? selectedPlayers;
/// List of available rulesets with their localized string representations
late final List<(Ruleset, String)> _rulesets;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -89,9 +91,18 @@ class _CreateMatchViewState extends State<CreateMatchView> {
}); });
} }
List<(Ruleset, String)> _getRulesets(BuildContext context) { @override
void dispose() {
_matchNameController.dispose();
super.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
return [ hintText ??= loc.match_name;
_rulesets = [
(Ruleset.singleWinner, loc.ruleset_single_winner), (Ruleset.singleWinner, loc.ruleset_single_winner),
(Ruleset.singleLoser, loc.ruleset_single_loser), (Ruleset.singleLoser, loc.ruleset_single_loser),
(Ruleset.mostPoints, loc.ruleset_most_points), (Ruleset.mostPoints, loc.ruleset_most_points),
@@ -110,24 +121,16 @@ class _CreateMatchViewState extends State<CreateMatchView> {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
return Scaffold( return Scaffold(
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar( appBar: AppBar(title: Text(loc.create_new_match)),
backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0,
title: Text(
loc.create_new_match,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
centerTitle: true,
),
body: SafeArea( body: SafeArea(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
Container( Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), margin: CustomTheme.tileMargin,
child: TextInputField( child: TextInputField(
controller: _matchNameController, controller: _matchNameController,
hintText: hintText, hintText: hintText ?? '',
), ),
), ),
ChooseTile( ChooseTile(
@@ -148,11 +151,11 @@ class _CreateMatchViewState extends State<CreateMatchView> {
if (selectedGameIndex != -1) { if (selectedGameIndex != -1) {
hintText = games[selectedGameIndex].$1; hintText = games[selectedGameIndex].$1;
selectedRuleset = games[selectedGameIndex].$3; selectedRuleset = games[selectedGameIndex].$3;
selectedRulesetIndex = _getRulesets( selectedRulesetIndex = _rulesets.indexWhere(
context, (r) => r.$1 == selectedRuleset,
).indexWhere((r) => r.$1 == selectedRuleset); );
} else { } else {
hintText = 'Match Name'; hintText = loc.match_name;
selectedRuleset = null; selectedRuleset = null;
} }
}); });
@@ -164,17 +167,16 @@ class _CreateMatchViewState extends State<CreateMatchView> {
? loc.none ? loc.none
: translateRulesetToString(selectedRuleset!, context), : translateRulesetToString(selectedRuleset!, context),
onPressed: () async { onPressed: () async {
final rulesets = _getRulesets(context);
selectedRuleset = await Navigator.of(context).push( selectedRuleset = await Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ChooseRulesetView( builder: (context) => ChooseRulesetView(
rulesets: rulesets, rulesets: _rulesets,
initialRulesetIndex: selectedRulesetIndex, initialRulesetIndex: selectedRulesetIndex,
), ),
), ),
); );
if (!mounted) return; if (!mounted) return;
selectedRulesetIndex = rulesets.indexWhere( selectedRulesetIndex = _rulesets.indexWhere(
(r) => r.$1 == selectedRuleset, (r) => r.$1 == selectedRuleset,
); );
selectedGameIndex = -1; selectedGameIndex = -1;
@@ -228,7 +230,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
? () async { ? () async {
Match match = Match( Match match = Match(
name: _matchNameController.text.isEmpty name: _matchNameController.text.isEmpty
? hintText ? (hintText ?? '')
: _matchNameController.text.trim(), : _matchNameController.text.trim(),
createdAt: DateTime.now(), createdAt: DateTime.now(),
group: selectedGroup, group: selectedGroup,
@@ -256,11 +258,14 @@ class _CreateMatchViewState extends State<CreateMatchView> {
); );
} }
/// Determines whether the "Create Game" button should be enabled based on /// Determines whether the "Create Match" button should be enabled.
/// the current state of the input fields. ///
/// Returns `true` if:
/// - A ruleset is selected AND
/// - Either a group is selected OR at least 2 players are selected
bool _enableCreateGameButton() { bool _enableCreateGameButton() {
return selectedGroup != null || return (selectedGroup != null ||
(selectedPlayers != null && selectedPlayers!.length > 1) && (selectedPlayers != null && selectedPlayers!.length > 1)) &&
selectedRuleset != null; selectedRuleset != null;
} }
} }

View File

@@ -18,8 +18,12 @@ class MatchResultView extends StatefulWidget {
} }
class _MatchResultViewState extends State<MatchResultView> { class _MatchResultViewState extends State<MatchResultView> {
late final List<Player> allPlayers;
late final AppDatabase db; late final AppDatabase db;
/// List of all players who participated in the match
late final List<Player> allPlayers;
/// Currently selected winner player
Player? _selectedPlayer; Player? _selectedPlayer;
@override @override
@@ -47,17 +51,7 @@ class _MatchResultViewState extends State<MatchResultView> {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),
backgroundColor: CustomTheme.backgroundColor, title: Text(widget.match.name),
scrolledUnderElevation: 0,
title: Text(
widget.match.name,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
overflow: TextOverflow.ellipsis,
),
),
centerTitle: true,
), ),
flixcoo marked this conversation as resolved
Review

grafik.png

![grafik.png](/attachments/06706384-fe63-4ea8-8886-07737018c7f5)
body: SafeArea( body: SafeArea(
child: Column( child: Column(
@@ -132,6 +126,8 @@ class _MatchResultViewState extends State<MatchResultView> {
); );
} }
/// Handles saving or removing the winner in the database
/// based on the current selection.
Future<void> _handleWinnerSaving() async { Future<void> _handleWinnerSaving() async {
if (_selectedPlayer == null) { if (_selectedPlayer == null) {
await db.matchDao.removeWinner(matchId: widget.match.id); await db.matchDao.removeWinner(matchId: widget.match.id);
@@ -144,6 +140,10 @@ class _MatchResultViewState extends State<MatchResultView> {
widget.onWinnerChanged?.call(); widget.onWinnerChanged?.call();
} }
/// Retrieves all players associated with the given [match].
/// This includes players directly assigned to the match
/// as well as members of the group (if any).
/// The returned list is sorted alphabetically by player name.
List<Player> getAllPlayers(Match match) { List<Player> getAllPlayers(Match match) {
List<Player> players = []; List<Player> players = [];

View File

@@ -28,6 +28,8 @@ class _MatchViewState extends State<MatchView> {
late final AppDatabase db; late final AppDatabase db;
flixcoo marked this conversation as resolved
Review

kein comment?

kein comment?
bool isLoading = true; bool isLoading = true;
flixcoo marked this conversation as resolved
Review

kein comment?

kein comment?
/// Loaded matches from the database,
/// initially filled with skeleton matches
List<Match> matches = List.filled( List<Match> matches = List.filled(
4, 4,
Match( Match(
@@ -44,7 +46,6 @@ class _MatchViewState extends State<MatchView> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
db = Provider.of<AppDatabase>(context, listen: false); db = Provider.of<AppDatabase>(context, listen: false);
loadGames(); loadGames();
} }
@@ -117,10 +118,11 @@ class _MatchViewState extends State<MatchView> {
); );
} }
/// Loads the games from the database and sorts them by creation date.
void loadGames() { void loadGames() {
Future.wait([ Future.wait([
db.matchDao.getAllMatches(), db.matchDao.getAllMatches(),
Future.delayed(minimumSkeletonDuration), Future.delayed(Constants.minimumSkeletonDuration),
]).then((results) { ]).then((results) {
if (mounted) { if (mounted) {
setState(() { setState(() {

View File

@@ -25,28 +25,7 @@ class _StatisticsViewState extends State<StatisticsView> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
loadStatisticData();
final db = Provider.of<AppDatabase>(context, listen: false);
Future.wait([
db.matchDao.getAllMatches(),
db.playerDao.getAllPlayers(),
Future.delayed(minimumSkeletonDuration),
]).then((results) async {
if (!mounted) return;
final matches = results[0] as List<Match>;
final players = results[1] as List<Player>;
winCounts = _calculateWinsForAllPlayers(matches, players, context);
matchCounts = _calculateMatchAmountsForAllPlayers(
matches,
players,
context,
);
winRates = computeWinRatePercent(wins: winCounts, matches: matchCounts);
setState(() {
isLoading = false;
});
});
} }
@override @override
@@ -78,7 +57,7 @@ class _StatisticsViewState extends State<StatisticsView> {
width: constraints.maxWidth * 0.95, width: constraints.maxWidth * 0.95,
values: winCounts, values: winCounts,
itemCount: 3, itemCount: 3,
barColor: Colors.blue, barColor: Colors.green,
), ),
SizedBox(height: constraints.maxHeight * 0.02), SizedBox(height: constraints.maxHeight * 0.02),
StatisticsTile( StatisticsTile(
@@ -96,7 +75,7 @@ class _StatisticsViewState extends State<StatisticsView> {
width: constraints.maxWidth * 0.95, width: constraints.maxWidth * 0.95,
values: matchCounts, values: matchCounts,
itemCount: 10, itemCount: 10,
barColor: Colors.green, barColor: Colors.blue,
), ),
], ],
), ),
@@ -118,13 +97,43 @@ class _StatisticsViewState extends State<StatisticsView> {
); );
} }
/// Loads matches and players from the database
/// and calculates statistics for each player
void loadStatisticData() {
final db = Provider.of<AppDatabase>(context, listen: false);
Future.wait([
db.matchDao.getAllMatches(),
db.playerDao.getAllPlayers(),
Future.delayed(Constants.minimumSkeletonDuration),
]).then((results) async {
if (!mounted) return;
final matches = results[0] as List<Match>;
final players = results[1] as List<Player>;
winCounts = _calculateWinsForAllPlayers(
matches: matches,
players: players,
context: context,
);
matchCounts = _calculateMatchAmountsForAllPlayers(
matches: matches,
players: players,
context: context,
);
winRates = computeWinRatePercent(wins: winCounts, matches: matchCounts);
setState(() {
isLoading = false;
});
});
}
/// Calculates the number of wins for each player /// Calculates the number of wins for each player
/// and returns a sorted list of tuples (playerName, winCount) /// and returns a sorted list of tuples (playerName, winCount)
List<(String, int)> _calculateWinsForAllPlayers( List<(String, int)> _calculateWinsForAllPlayers({
List<Match> matches, required List<Match> matches,
List<Player> players, required List<Player> players,
BuildContext context, required BuildContext context,
) { }) {
List<(String, int)> winCounts = []; List<(String, int)> winCounts = [];
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
@@ -169,11 +178,11 @@ class _StatisticsViewState extends State<StatisticsView> {
/// Calculates the number of matches played for each player /// Calculates the number of matches played for each player
/// and returns a sorted list of tuples (playerName, matchCount) /// and returns a sorted list of tuples (playerName, matchCount)
List<(String, int)> _calculateMatchAmountsForAllPlayers( List<(String, int)> _calculateMatchAmountsForAllPlayers({
List<Match> matches, required List<Match> matches,
List<Player> players, required List<Player> players,
BuildContext context, required BuildContext context,
) { }) {
List<(String, int)> matchCounts = []; List<(String, int)> matchCounts = [];
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);

View File

@@ -1,11 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
/// A widget that provides a skeleton loading effect to its child widget tree.
/// - [child]: The widget tree to apply the skeleton effect to.
/// - [enabled]: A boolean to enable or disable the skeleton effect.
/// - [fixLayoutBuilder]: A boolean to fix the layout builder for AnimatedSwitcher.
class AppSkeleton extends StatefulWidget { class AppSkeleton extends StatefulWidget {
final Widget child;
final bool enabled;
final bool fixLayoutBuilder;
const AppSkeleton({ const AppSkeleton({
super.key, super.key,
required this.child, required this.child,
@@ -13,6 +13,15 @@ class AppSkeleton extends StatefulWidget {
this.fixLayoutBuilder = false, this.fixLayoutBuilder = false,
}); });
/// The widget tree to apply the skeleton effect to.
final Widget child;
/// A boolean to enable or disable the skeleton effect.
final bool enabled;
/// A boolean to fix the layout builder for AnimatedSwitcher.
final bool fixLayoutBuilder;
@override @override
State<AppSkeleton> createState() => _AppSkeletonState(); State<AppSkeleton> createState() => _AppSkeletonState();
} }

View File

@@ -2,6 +2,12 @@ import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart'; import 'package:game_tracker/core/enums.dart';
/// A custom button widget that is designed to have a width relative to the screen size.
/// It supports three types of buttons: primary, secondary, and text buttons.
/// - [text]: The text to display on the button.
/// - [buttonType]: The type of button to display. Defaults to [ButtonType.primary].
/// - [sizeRelativeToWidth]: The size of the button relative to the width of the screen.
/// - [onPressed]: The callback to be invoked when the button is pressed.
class CustomWidthButton extends StatelessWidget { class CustomWidthButton extends StatelessWidget {
const CustomWidthButton({ const CustomWidthButton({
super.key, super.key,
@@ -11,9 +17,16 @@ class CustomWidthButton extends StatelessWidget {
this.onPressed, this.onPressed,
}); });
/// The text to display on the button.
final String text; final String text;
/// The size of the button relative to the width of the screen.
final double sizeRelativeToWidth; final double sizeRelativeToWidth;
/// The callback to be invoked when the button is pressed.
final VoidCallback? onPressed; final VoidCallback? onPressed;
/// The type of button to display. Depends on the enum [ButtonType].
final ButtonType buttonType; final ButtonType buttonType;
@override @override
@@ -47,7 +60,7 @@ class CustomWidthButton extends StatelessWidget {
60, 60,
), ),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: CustomTheme.standardBorderRadiusAll,
), ),
), ),
child: Text( child: Text(
@@ -78,7 +91,7 @@ class CustomWidthButton extends StatelessWidget {
), ),
side: BorderSide(color: borderSideColor, width: 2), side: BorderSide(color: borderSideColor, width: 2),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: CustomTheme.standardBorderRadiusAll,
), ),
), ),
child: Text( child: Text(

View File

@@ -1,15 +1,22 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
/// A button widget designed for quick creating matches in the [HomeView]
/// - [text]: The text to display on the button.
/// - [onPressed]: The callback to be invoked when the button is pressed.
class QuickCreateButton extends StatefulWidget { class QuickCreateButton extends StatefulWidget {
final String text;
final VoidCallback? onPressed;
const QuickCreateButton({ const QuickCreateButton({
super.key, super.key,
required this.text, required this.text,
required this.onPressed, required this.onPressed,
}); });
/// The text to display on the button.
final String text;
/// The callback to be invoked when the button is pressed.
final VoidCallback? onPressed;
@override @override
State<QuickCreateButton> createState() => _QuickCreateButtonState(); State<QuickCreateButton> createState() => _QuickCreateButtonState();
} }
@@ -22,7 +29,9 @@ class _QuickCreateButtonState extends State<QuickCreateButton> {
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
minimumSize: const Size(140, 45), minimumSize: const Size(140, 45),
backgroundColor: CustomTheme.primaryColor, backgroundColor: CustomTheme.primaryColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(
borderRadius: CustomTheme.standardBorderRadiusAll,
),
), ),
child: Text( child: Text(
widget.text, widget.text,

View File

@@ -1,12 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// A navigation bar item widget that represents a single tab in a navigation bar.
/// - [index]: The index of the tab.
/// - [isSelected]: A boolean indicating whether the tab is currently selected.
/// - [icon]: The icon to display for the tab.
/// - [label]: The label to display for the tab.
/// - [onTabTapped]: The callback to be invoked when the tab is tapped.
class NavbarItem extends StatefulWidget { class NavbarItem extends StatefulWidget {
final int index;
final bool isSelected;
final IconData icon;
final String label;
final Function(int) onTabTapped;
const NavbarItem({ const NavbarItem({
super.key, super.key,
required this.index, required this.index,
@@ -16,6 +16,21 @@ class NavbarItem extends StatefulWidget {
required this.onTabTapped, required this.onTabTapped,
}); });
/// The index of the tab.
final int index;
/// A boolean indicating whether the tab is currently selected.
final bool isSelected;
/// The icon to display for the tab.
final IconData icon;
/// The label to display for the tab.
final String label;
/// The callback to be invoked when the tab is tapped.
final Function(int) onTabTapped;
@override @override
State<NavbarItem> createState() => _NavbarItemState(); State<NavbarItem> createState() => _NavbarItemState();
} }

View File

@@ -11,31 +11,55 @@ import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
/// A widget that allows users to select players from a list,
/// with search functionality and the ability to add new players.
/// - [availablePlayers]: An optional list of players to choose from. If null, all
/// players from the database are used.
/// - [initialSelectedPlayers]: An optional list of players that should be pre-selected.
/// - [onChanged]: A callback function that is invoked whenever the selection changes,
/// providing the updated list of selected players.
class PlayerSelection extends StatefulWidget { class PlayerSelection extends StatefulWidget {
final Function(List<Player> value) onChanged;
final List<Player>? availablePlayers;
final List<Player>? initialSelectedPlayers;
const PlayerSelection({ const PlayerSelection({
super.key, super.key,
required this.onChanged,
this.availablePlayers, this.availablePlayers,
this.initialSelectedPlayers, this.initialSelectedPlayers,
required this.onChanged,
}); });
/// An optional list of players to choose from. If null, all players from the database are used.
final List<Player>? availablePlayers;
/// An optional list of players that should be pre-selected.
final List<Player>? initialSelectedPlayers;
/// A callback function that is invoked whenever the selection changes,
final Function(List<Player> value) onChanged;
@override @override
State<PlayerSelection> createState() => _PlayerSelectionState(); State<PlayerSelection> createState() => _PlayerSelectionState();
} }
class _PlayerSelectionState extends State<PlayerSelection> { class _PlayerSelectionState extends State<PlayerSelection> {
List<Player> selectedPlayers = []; late final AppDatabase db;
flixcoo marked this conversation as resolved
Review

kein comment?

kein comment?
List<Player> suggestedPlayers = [];
List<Player> allPlayers = [];
bool isLoading = true; bool isLoading = true;
flixcoo marked this conversation as resolved
Review

kein comment?

kein comment?
/// Future that loads all players from the database.
late Future<List<Player>> _allPlayersFuture;
/// The complete list of all available players.
List<Player> allPlayers = [];
/// The list of players suggested based on the search input.
List<Player> suggestedPlayers = [];
/// The list of currently selected players.
List<Player> selectedPlayers = [];
/// Controller for the search bar input.
late final TextEditingController _searchBarController = late final TextEditingController _searchBarController =
TextEditingController(); TextEditingController();
late final AppDatabase db;
late Future<List<Player>> _allPlayersFuture; /// Skeleton data used while loading players.
late final List<Player> skeletonData = List.filled( late final List<Player> skeletonData = List.filled(
7, 7,
Player(name: 'Player 0'), Player(name: 'Player 0'),
@@ -49,47 +73,11 @@ class _PlayerSelectionState extends State<PlayerSelection> {
loadPlayerList(); loadPlayerList();
} }
void loadPlayerList() {
_allPlayersFuture = Future.wait([
db.playerDao.getAllPlayers(),
Future.delayed(minimumSkeletonDuration),
]).then((results) => results[0] as List<Player>);
if (mounted) {
_allPlayersFuture.then((loadedPlayers) {
setState(() {
// If a list of available players is provided (even if empty), use that list.
if (widget.availablePlayers != null) {
widget.availablePlayers!.sort((a, b) => a.name.compareTo(b.name));
allPlayers = [...widget.availablePlayers!];
suggestedPlayers = [...allPlayers];
if (widget.initialSelectedPlayers != null) {
// Ensures that only players available for selection are pre-selected.
selectedPlayers = widget.initialSelectedPlayers!
.where(
(p) => widget.availablePlayers!.any(
(available) => available.id == p.id,
),
)
.toList();
}
} else {
// Otherwise, use the loaded players from the database.
loadedPlayers.sort((a, b) => a.name.compareTo(b.name));
allPlayers = [...loadedPlayers];
suggestedPlayers = [...loadedPlayers];
}
isLoading = false;
});
});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), margin: CustomTheme.standardMargin,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10),
decoration: CustomTheme.standardBoxDecoration, decoration: CustomTheme.standardBoxDecoration,
child: Column( child: Column(
@@ -131,9 +119,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
AppLocalizations.of( loc.selected_players,
context,
).selected_players(selectedPlayers.length),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
@@ -227,51 +213,95 @@ class _PlayerSelectionState extends State<PlayerSelection> {
); );
} }
/// Loads the list of players from the database or uses the provided available players.
/// Sets the loading state and updates the player lists accordingly.
void loadPlayerList() {
_allPlayersFuture = Future.wait([
db.playerDao.getAllPlayers(),
Future.delayed(Constants.minimumSkeletonDuration),
]).then((results) => results[0] as List<Player>);
if (mounted) {
_allPlayersFuture.then((loadedPlayers) {
setState(() {
// If a list of available players is provided (even if empty), use that list.
if (widget.availablePlayers != null) {
widget.availablePlayers!.sort((a, b) => a.name.compareTo(b.name));
allPlayers = [...widget.availablePlayers!];
suggestedPlayers = [...allPlayers];
if (widget.initialSelectedPlayers != null) {
// Ensures that only players available for selection are pre-selected.
selectedPlayers = widget.initialSelectedPlayers!
.where(
(p) => widget.availablePlayers!.any(
(available) => available.id == p.id,
),
)
.toList();
}
} else {
// Otherwise, use the loaded players from the database.
loadedPlayers.sort((a, b) => a.name.compareTo(b.name));
allPlayers = [...loadedPlayers];
suggestedPlayers = [...loadedPlayers];
}
isLoading = false;
});
});
}
}
/// Adds a new player to the database from the search bar input. /// Adds a new player to the database from the search bar input.
/// Shows a snackbar indicating success or failure. /// Shows a snackbar indicating success or failure.
/// [context] - BuildContext to show the snackbar. /// [context] - BuildContext to show the snackbar.
void addNewPlayerFromSearch({required BuildContext context}) async { Future<void> addNewPlayerFromSearch({required BuildContext context}) async {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
String playerName = _searchBarController.text.trim(); final playerName = _searchBarController.text.trim();
Player createdPlayer = Player(name: playerName);
bool success = await db.playerDao.addPlayer(player: createdPlayer); final createdPlayer = Player(name: playerName);
final success = await db.playerDao.addPlayer(player: createdPlayer);
if (!context.mounted) return; if (!context.mounted) return;
if (success) { if (success) {
selectedPlayers.insert(0, createdPlayer); _handleSuccessfulPlayerCreation(createdPlayer);
showSnackBarMessage(loc.successfully_added_player(playerName));
} else {
showSnackBarMessage(loc.could_not_add_player(playerName));
}
}
/// Updates the state after successfully adding a new player.
void _handleSuccessfulPlayerCreation(Player player) {
selectedPlayers.insert(0, player);
widget.onChanged([...selectedPlayers]); widget.onChanged([...selectedPlayers]);
allPlayers.add(createdPlayer); allPlayers.add(player);
setState(() { setState(() {
_searchBarController.clear(); _searchBarController.clear();
suggestedPlayers = allPlayers.where((player) { _updateSuggestedPlayers();
return !selectedPlayers.contains(player);
}).toList();
}); });
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: CustomTheme.boxColor,
content: Center(
child: Text(
AppLocalizations.of(
context,
).successfully_added_player(playerName),
style: const TextStyle(color: Colors.white),
),
),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: CustomTheme.boxColor,
content: Center(
child: Text(
loc.could_not_add_player(playerName),
style: const TextStyle(color: Colors.white),
),
),
),
);
} }
/// Updates the suggested players list based on current selection.
void _updateSuggestedPlayers() {
suggestedPlayers = allPlayers
.where((player) => !selectedPlayers.contains(player))
.toList();
}
/// Displays a snackbar message at the bottom of the screen.
/// [message] - The message to display in the snackbar.
void showSnackBarMessage(String message) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: CustomTheme.boxColor,
content: Center(
child: Text(message, style: const TextStyle(color: Colors.white)),
),
),
);
} }
/// Determines the appropriate info text to display when no players /// Determines the appropriate info text to display when no players

View File

@@ -1,16 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
/// A custom search bar widget that encapsulates a [SearchBar] with additional customization options.
/// - [controller]: The controller for the search bar's text input.
/// - [hintText]: The hint text displayed in the search bar when it is empty.
/// - [trailingButtonShown]: Whether to show the trailing button.
/// - [trailingButtonicon]: The icon for the trailing button.
/// - [trailingButtonEnabled]: Whether the trailing button is in enabled state.
/// - [onTrailingButtonPressed]: The callback invoked when the trailing button is pressed.
/// - [onChanged]: The callback invoked when the text in the search bar changes.
/// - [constraints]: The constraints for the search bar.
class CustomSearchBar extends StatelessWidget { class CustomSearchBar extends StatelessWidget {
final TextEditingController controller;
final String hintText;
final ValueChanged<String>? onChanged;
final BoxConstraints? constraints;
final bool trailingButtonShown;
final bool trailingButtonEnabled;
final VoidCallback? onTrailingButtonPressed;
final IconData trailingButtonicon;
const CustomSearchBar({ const CustomSearchBar({
super.key, super.key,
required this.controller, required this.controller,
@@ -23,6 +23,30 @@ class CustomSearchBar extends StatelessWidget {
this.constraints, this.constraints,
}); });
/// The controller for the search bar's text input.
final TextEditingController controller;
/// The hint text displayed in the search bar when it is empty.
final String hintText;
/// Whether to show the trailing button.
final bool trailingButtonShown;
/// The icon for the trailing button.
final IconData trailingButtonicon;
/// Whether the trailing button is in enabled state.
final bool trailingButtonEnabled;
/// The callback invoked when the trailing button is pressed.
final VoidCallback? onTrailingButtonPressed;
/// The callback invoked when the text in the search bar changes.
final ValueChanged<String>? onChanged;
/// The constraints for the search bar.
final BoxConstraints? constraints;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SearchBar( return SearchBar(

View File

@@ -1,11 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
/// A custom text input field widget that encapsulates a [TextField] with specific styling.
/// - [controller]: The controller for the text input field.
/// - [onChanged]: The callback invoked when the text in the field changes.
/// - [hintText]: The hint text displayed in the text input field when it is empty
class TextInputField extends StatelessWidget { class TextInputField extends StatelessWidget {
final TextEditingController controller;
final ValueChanged<String>? onChanged;
final String hintText;
const TextInputField({ const TextInputField({
super.key, super.key,
required this.controller, required this.controller,
@@ -13,6 +13,15 @@ class TextInputField extends StatelessWidget {
this.onChanged, this.onChanged,
}); });
/// The controller for the text input field.
final TextEditingController controller;
/// The callback invoked when the text in the field changes.
final ValueChanged<String>? onChanged;
/// The hint text displayed in the text input field when it is empty.
final String hintText;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TextField( return TextField(

View File

@@ -1,10 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
/// A tile widget that allows users to choose an option by tapping on it.
/// - [title]: The title text displayed on the tile.
/// - [trailingText]: Optional trailing text displayed on the tile.
/// - [onPressed]: The callback invoked when the tile is tapped.
class ChooseTile extends StatefulWidget { class ChooseTile extends StatefulWidget {
final String title;
final VoidCallback? onPressed;
final String? trailingText;
const ChooseTile({ const ChooseTile({
super.key, super.key,
required this.title, required this.title,
@@ -12,6 +13,15 @@ class ChooseTile extends StatefulWidget {
this.onPressed, this.onPressed,
}); });
/// The title text displayed on the tile.
final String title;
/// The callback invoked when the tile is tapped.
final VoidCallback? onPressed;
/// Optional trailing text displayed on the tile.
final String? trailingText;
@override @override
State<ChooseTile> createState() => _ChooseTileState(); State<ChooseTile> createState() => _ChooseTileState();
} }
@@ -22,8 +32,8 @@ class _ChooseTileState extends State<ChooseTile> {
return GestureDetector( return GestureDetector(
onTap: widget.onPressed, onTap: widget.onPressed,
child: Container( child: Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), margin: CustomTheme.tileMargin,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: CustomTheme.standardBoxDecoration, decoration: CustomTheme.standardBoxDecoration,
child: Row( child: Row(
children: [ children: [

View File

@@ -1,11 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
/// A custom radio list tile widget that encapsulates a [Radio] button with additional styling and functionality.
/// - [text]: The text to display next to the radio button.
/// - [value]: The value associated with the radio button.
/// - [onContainerTap]: The callback invoked when the container is tapped.
class CustomRadioListTile<T> extends StatelessWidget { class CustomRadioListTile<T> extends StatelessWidget {
final String text;
final T value;
final ValueChanged<T> onContainerTap;
const CustomRadioListTile({ const CustomRadioListTile({
super.key, super.key,
required this.text, required this.text,
@@ -13,6 +13,15 @@ class CustomRadioListTile<T> extends StatelessWidget {
required this.onContainerTap, required this.onContainerTap,
}); });
/// The text to display next to the radio button.
final String text;
/// The value associated with the radio button.
final T value;
/// The callback invoked when the container is tapped.
final ValueChanged<T> onContainerTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
@@ -23,7 +32,7 @@ class CustomRadioListTile<T> extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: CustomTheme.boxColor, color: CustomTheme.boxColor,
border: Border.all(color: CustomTheme.boxBorder), border: Border.all(color: CustomTheme.boxBorder),
borderRadius: BorderRadius.circular(12), borderRadius: CustomTheme.standardBorderRadiusAll,
), ),
child: Row( child: Row(
children: [ children: [

View File

@@ -3,16 +3,22 @@ import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/data/dto/group.dart'; import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
/// A tile widget that displays information about a group, including its name and members.
/// - [group]: The group data to be displayed.
/// - [isHighlighted]: Whether the tile should be highlighted.
class GroupTile extends StatelessWidget { class GroupTile extends StatelessWidget {
const GroupTile({super.key, required this.group, this.isHighlighted = false}); const GroupTile({super.key, required this.group, this.isHighlighted = false});
/// The group data to be displayed.
final Group group; final Group group;
/// Whether the tile should be highlighted.
final bool isHighlighted; final bool isHighlighted;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnimatedContainer( return AnimatedContainer(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), margin: CustomTheme.standardMargin,
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
decoration: isHighlighted decoration: isHighlighted
? CustomTheme.highlightedBoxDecoration ? CustomTheme.highlightedBoxDecoration

View File

@@ -1,13 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
/// A tile widget that displays a title with an icon and some content below it.
/// - [title]: The title text displayed on the tile.
/// - [icon]: The icon displayed next to the title.
/// - [content]: The content widget displayed below the title.
/// - [padding]: Optional padding for the tile content.
/// - [height]: Optional height for the tile.
/// - [width]: Optional width for the tile.
class InfoTile extends StatefulWidget { class InfoTile extends StatefulWidget {
final String title;
final IconData icon;
final Widget content;
final EdgeInsets? padding;
final double? height;
final double? width;
const InfoTile({ const InfoTile({
super.key, super.key,
required this.title, required this.title,
@@ -18,6 +19,24 @@ class InfoTile extends StatefulWidget {
this.width, this.width,
}); });
/// The title text displayed on the tile.
final String title;
/// The icon displayed next to the title.
final IconData icon;
/// The content widget displayed below the title.
final Widget content;
/// Optional padding for the tile content.
final EdgeInsets? padding;
/// Optional height for the tile.
final double? height;
/// Optional width for the tile.
final double? width;
@override @override
State<InfoTile> createState() => _InfoTileState(); State<InfoTile> createState() => _InfoTileState();
} }

View File

@@ -1,33 +1,48 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/data/dto/match.dart'; import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
/// A tile widget that displays information about a match, including its name,
/// creation date, associated group, winner, and players.
/// - [match]: The match data to be displayed.
/// - [onTap]: The callback invoked when the tile is tapped.
class MatchTile extends StatefulWidget { class MatchTile extends StatefulWidget {
final Match match;
final VoidCallback onTap;
const MatchTile({super.key, required this.match, required this.onTap}); const MatchTile({super.key, required this.match, required this.onTap});
/// The match data to be displayed.
final Match match;
/// The callback invoked when the tile is tapped.
final VoidCallback onTap;
@override @override
State<MatchTile> createState() => _MatchTileState(); State<MatchTile> createState() => _MatchTileState();
} }
class _MatchTileState extends State<MatchTile> { class _MatchTileState extends State<MatchTile> {
late final List<Player> _allPlayers;
@override
void initState() {
super.initState();
_allPlayers = _getCombinedPlayers();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final group = widget.match.group; final group = widget.match.group;
final winner = widget.match.winner; final winner = widget.match.winner;
final allPlayers = _getAllPlayers();
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
return GestureDetector( return GestureDetector(
onTap: widget.onTap, onTap: widget.onTap,
child: Container( child: Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), margin: CustomTheme.tileMargin,
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: CustomTheme.boxColor, color: CustomTheme.boxColor,
border: Border.all(color: CustomTheme.boxBorder), border: Border.all(color: CustomTheme.boxBorder),
@@ -103,7 +118,7 @@ class _MatchTileState extends State<MatchTile> {
style: const TextStyle( style: const TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Colors.white, color: CustomTheme.textColor,
), ),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -114,7 +129,7 @@ class _MatchTileState extends State<MatchTile> {
const SizedBox(height: 12), const SizedBox(height: 12),
], ],
if (allPlayers.isNotEmpty) ...[ if (_allPlayers.isNotEmpty) ...[
Text( Text(
loc.players, loc.players,
style: const TextStyle( style: const TextStyle(
@@ -127,7 +142,7 @@ class _MatchTileState extends State<MatchTile> {
Wrap( Wrap(
spacing: 6, spacing: 6,
runSpacing: 6, runSpacing: 6,
children: allPlayers.map((player) { children: _allPlayers.map((player) {
return TextIconTile(text: player.name, iconEnabled: false); return TextIconTile(text: player.name, iconEnabled: false);
}).toList(), }).toList(),
), ),
@@ -138,19 +153,17 @@ class _MatchTileState extends State<MatchTile> {
); );
} }
/// Formats the given [dateTime] into a human-readable string based on its
/// difference from the current date.
String _formatDate(DateTime dateTime, BuildContext context) { String _formatDate(DateTime dateTime, BuildContext context) {
final now = DateTime.now(); final now = DateTime.now();
final difference = now.difference(dateTime); final difference = now.difference(dateTime);
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
if (difference.inDays == 0) { if (difference.inDays == 0) {
return AppLocalizations.of( return "${loc.today_at} ${DateFormat('HH:mm').format(dateTime)}";
context,
).today_at(DateFormat('HH:mm').format(dateTime));
} else if (difference.inDays == 1) { } else if (difference.inDays == 1) {
return AppLocalizations.of( return "${loc.yesterday_at} ${DateFormat('HH:mm').format(dateTime)}";
context,
).yesterday_at(DateFormat('HH:mm').format(dateTime));
} else if (difference.inDays < 7) { } else if (difference.inDays < 7) {
return loc.days_ago(difference.inDays); return loc.days_ago(difference.inDays);
} else { } else {
@@ -158,8 +171,10 @@ class _MatchTileState extends State<MatchTile> {
} }
} }
List<dynamic> _getAllPlayers() { /// Retrieves all unique players associated with the match,
final allPlayers = <dynamic>[]; /// combining players from both the match and its group.
List<Player> _getCombinedPlayers() {
final allPlayers = <Player>[];
final playerIds = <String>{}; final playerIds = <String>{};
// Add players from game.players // Add players from game.players

View File

@@ -1,13 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
/// A tile widget that displays a title with an icon and a numeric value below it.
/// - [title]: The title text displayed on the tile.
/// - [icon]: The icon displayed next to the title.
/// - [value]: The numeric value displayed below the title.
/// - [height]: Optional height for the tile.
/// - [width]: Optional width for the tile.
/// - [padding]: Optional padding for the tile content.
class QuickInfoTile extends StatefulWidget { class QuickInfoTile extends StatefulWidget {
final String title;
final IconData icon;
final int value;
final double? height;
final double? width;
final EdgeInsets? padding;
const QuickInfoTile({ const QuickInfoTile({
super.key, super.key,
required this.title, required this.title,
@@ -18,6 +19,24 @@ class QuickInfoTile extends StatefulWidget {
this.padding, this.padding,
}); });
/// The title text displayed on the tile.
final String title;
/// The icon displayed next to the title.
final IconData icon;
/// The numeric value displayed below the title.
final int value;
/// Optional height for the tile.
final double? height;
/// Optional width for the tile.
final double? width;
/// Optional padding for the tile content.
final EdgeInsets? padding;
@override @override
State<QuickInfoTile> createState() => _QuickInfoTileState(); State<QuickInfoTile> createState() => _QuickInfoTileState();
} }

View File

@@ -1,19 +1,32 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
/// A customizable settings list tile widget that displays an icon, title, and an optional suffix widget.
/// - [icon]: The icon displayed on the left side of the tile.
/// - [title]: The title text displayed next to the icon.
/// - [suffixWidget]: An optional widget displayed on the right side of the tile.
/// - [onPressed]: The callback invoked when the tile is tapped.
class SettingsListTile extends StatelessWidget { class SettingsListTile extends StatelessWidget {
final VoidCallback? onPressed;
final IconData icon;
final String title;
final Widget? suffixWidget;
const SettingsListTile({ const SettingsListTile({
super.key, super.key,
required this.title,
required this.icon, required this.icon,
required this.title,
this.suffixWidget, this.suffixWidget,
this.onPressed, this.onPressed,
}); });
/// The icon displayed on the left side of the tile.
final IconData icon;
/// The title text displayed next to the icon.
final String title;
/// An optional widget displayed on the right side of the tile.
final Widget? suffixWidget;
/// The callback invoked when the tile is tapped.
final VoidCallback? onPressed;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(

View File

@@ -4,6 +4,13 @@ import 'package:flutter/material.dart';
import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart';
/// A tile widget that displays statistical data using horizontal bars.
/// - [icon]: The icon displayed next to the title.
/// - [title]: The title text displayed on the tile.
/// - [width]: The width of the tile.
/// - [values]: A list of tuples containing labels and their corresponding numeric values.
/// - [itemCount]: The maximum number of items to display.
/// - [barColor]: The color of the bars representing the values.
class StatisticsTile extends StatelessWidget { class StatisticsTile extends StatelessWidget {
const StatisticsTile({ const StatisticsTile({
super.key, super.key,
@@ -15,16 +22,26 @@ class StatisticsTile extends StatelessWidget {
required this.barColor, required this.barColor,
}); });
/// The icon displayed next to the title.
final IconData icon; final IconData icon;
/// The title text displayed on the tile.
final String title; final String title;
/// The width of the tile.
final double width; final double width;
/// A list of tuples containing labels and their corresponding numeric values.
final List<(String, num)> values; final List<(String, num)> values;
/// The maximum number of items to display.
final int itemCount; final int itemCount;
/// The color of the bars representing the values.
final Color barColor; final Color barColor;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final maxBarWidth = MediaQuery.of(context).size.width * 0.65;
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
return InfoTile( return InfoTile(
@@ -39,7 +56,10 @@ class StatisticsTile extends StatelessWidget {
heightFactor: 4, heightFactor: 4,
child: Text(loc.no_data_available), child: Text(loc.no_data_available),
), ),
child: Column( child: LayoutBuilder(
builder: (context, constraints) {
final maxBarWidth = constraints.maxWidth * 0.65;
return Column(
children: List.generate(min(values.length, itemCount), (index) { children: List.generate(min(values.length, itemCount), (index) {
/// The maximum wins among all players /// The maximum wins among all players
final maxMatches = values.isNotEmpty ? values[0].$2 : 0; final maxMatches = values.isNotEmpty ? values[0].$2 : 0;
@@ -96,6 +116,8 @@ class StatisticsTile extends StatelessWidget {
), ),
); );
}), }),
);
},
), ),
), ),
), ),

View File

@@ -1,18 +1,27 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
/// A list tile widget that displays text with an optional icon button.
/// - [text]: The text to display in the tile.
/// - [onPressed]: The callback to be invoked when the icon is pressed.
/// - [iconEnabled]: A boolean to determine if the icon should be displayed.
class TextIconListTile extends StatelessWidget { class TextIconListTile extends StatelessWidget {
final String text;
final VoidCallback? onPressed;
final bool iconEnabled;
const TextIconListTile({ const TextIconListTile({
super.key, super.key,
required this.text, required this.text,
this.onPressed,
this.iconEnabled = true, this.iconEnabled = true,
this.onPressed,
}); });
/// The text to display in the tile.
final String text;
/// A boolean to determine if the icon should be displayed.
final bool iconEnabled;
/// The callback to be invoked when the icon is pressed.
final VoidCallback? onPressed;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(

View File

@@ -1,18 +1,27 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
/// A tile widget that displays text with an optional icon that can be tapped.
/// - [text]: The text to display in the tile.
/// - [iconEnabled]: A boolean to determine if the icon should be displayed.
/// - [onIconTap]: The callback to be invoked when the icon is tapped.
class TextIconTile extends StatelessWidget { class TextIconTile extends StatelessWidget {
final String text;
final bool iconEnabled;
final VoidCallback? onIconTap;
const TextIconTile({ const TextIconTile({
super.key, super.key,
required this.text, required this.text,
this.onIconTap,
this.iconEnabled = true, this.iconEnabled = true,
this.onIconTap,
}); });
/// The text to display in the tile.
final String text;
/// A boolean to determine if the icon should be displayed.
final bool iconEnabled;
/// The callback to be invoked when the icon is tapped.
final VoidCallback? onIconTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(

View File

@@ -1,14 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
/// A list tile widget that displays a title and description, with optional highlighting and badge.
/// - [title]: The title text displayed on the tile.
/// - [description]: The description text displayed below the title.
/// - [onPressed]: The callback invoked when the tile is tapped.
/// - [isHighlighted]: A boolean to determine if the tile should be highlighted.
/// - [badgeText]: Optional text to display in a badge on the right side of the title.
/// - [badgeColor]: Optional color for the badge background.
class TitleDescriptionListTile extends StatelessWidget { class TitleDescriptionListTile extends StatelessWidget {
final String title;
final String description;
final VoidCallback? onPressed;
final bool isHighlighted;
final String? badgeText;
final Color? badgeColor;
const TitleDescriptionListTile({ const TitleDescriptionListTile({
super.key, super.key,
required this.title, required this.title,
@@ -19,6 +19,24 @@ class TitleDescriptionListTile extends StatelessWidget {
this.badgeColor, this.badgeColor,
}); });
/// The title text displayed on the tile.
final String title;
/// The description text displayed below the title.
final String description;
/// The callback invoked when the tile is tapped.
final VoidCallback? onPressed;
/// A boolean to determine if the tile should be highlighted.
final bool isHighlighted;
/// Optional text to display in a badge on the right side of the title.
final String? badgeText;
/// Optional color for the badge background.
final Color? badgeColor;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(

View File

@@ -1,5 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// A widget that displays a message centered at the top of the screen with an icon, title, and message.
/// - [icon]: The icon to display above the title.
/// - [title]: The title text to display.
/// - [message]: The message text to display below the title.
class TopCenteredMessage extends StatelessWidget { class TopCenteredMessage extends StatelessWidget {
const TopCenteredMessage({ const TopCenteredMessage({
super.key, super.key,
@@ -8,10 +12,15 @@ class TopCenteredMessage extends StatelessWidget {
required this.message, required this.message,
}); });
final String title; /// The icon to display above the title.
final String message;
final IconData icon; final IconData icon;
/// The title text to display.
final String title;
/// The message text to display below the title.
final String message;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(